feat(docx): 跨平台启动器替换 bash 包装,复用预装依赖免每次安装 (#21)

## 概述 / Summary

把 docx 技能对"客户端预装运行时依赖"的复用方式从 **bash 包装脚本**改为**跨平台 runtime 启动器**,实现
Win/macOS/Linux 一致、不依赖 Git Bash,并修复若干 POSIX 硬编码导致的 Windows 崩溃点。

Switch the docx skill's reuse of client-preinstalled runtime deps from a
**bash wrapper** to **cross-platform runtime launchers**, so it behaves
identically on Win/macOS/Linux without Git Bash, and fix several
POSIX-hardcoded crashes on Windows.

## 改动 / Changes

- **新增 / Add** `scripts/preload-deps.cjs`(Node 预加载,注入 `NODE_PATH`)与
`scripts/with-deps.py`(Python 启动器,按需切换到内置含 lxml 的 Python);**删除** bash 版
`with-deps.sh`。
- 生成走 `node -r preload-deps.cjs`,office 脚本走 `python with-deps.py` ——
离线复用预装的 docx-js / defusedxml / lxml,免每次 `npm`/`pip install`,且**不依赖
bash**。
- `comment.py` 补 defusedxml sys.path shim;`validate.py` 修临时目录泄漏(atexit
清理)。
- `accept_changes.py` 去除 `/tmp` 硬编码(`tempfile.gettempdir` +
`Path.as_uri`);`soffice.py` 仅 Linux 启用 AF_UNIX shim,避免 Windows 崩溃。
- `SKILL.md` / `SKILL.zh-CN.md` 同步命令形式、加 ESM
警告与外部工具(pandoc/LibreOffice/poppler)跨平台安装指引,`source_hash` 重算。

## 测试 / Testing

- 真实 dev 根目录端到端:生成 docx(免安装)+ 完整 XSD 校验(含 lxml)+ unpack/pack 往返均通过。
- 仓库 `validate-i18n.py` 校验通过;全 py 脚本 `py_compile` + `preload-deps.cjs`
`node --check` 通过。

---

- [x] 我已阅读并同意 CLA / I have read and agree to the CLA

Co-authored-by: 张馨元 <zhangxy@iynss.com>
Co-authored-by: Yige <a@wyr.me>
This commit is contained in:
Zxy-y
2026-06-04 11:14:36 +08:00
committed by GitHub
parent b15fce19bf
commit 17fe79ab49
10 changed files with 276 additions and 77 deletions

View File

@@ -0,0 +1,74 @@
#!/usr/bin/env python3
"""with-deps.py —— 跨平台 Python 启动器(无需 bash对应已废弃的 with-deps.sh
让 office 脚本复用客户端预装的共享依赖,免去运行时 pip install
- defusedxml纯 Python注入 PYTHONPATH<ROOT>/runtime-deps/python-libs
- lxml编译型扩展绑定具体解释器 → 若存在受控 Python<ROOT>/runtime-deps/
python-runtime已装 lxml用它运行目标脚本从而离线启用完整 XSD 校验;
受控 Python 不存在 / 无法执行(如 macOS 公证拦截)→ 自动退回当前 Python
(此时 lxml 缺失,校验会优雅降级跳过,不会崩)。
纯 Python 实现,在 Windows / macOS / Linux 上用同一条命令运行,不依赖 bash
python "<skill-dir>/scripts/with-deps.py" office/unpack.py document.docx unpacked/
python "<skill-dir>/scripts/with-deps.py" office/validate.py doc.docx
目标脚本以 [解释器, 目标, *参数] 直接拉起 —— 等价于 `python <目标>`,因此脚本目录
会被 Python 自动加入 sys.path、__name__ == "__main__"、argv 与直接运行完全一致。
"""
import os
import subprocess
import sys
_HERE = os.path.dirname(os.path.abspath(__file__)) # .../skills/docx/scripts
# scripts → docx → skills → <ROOT>
_ROOT = os.path.abspath(os.path.join(_HERE, "..", "..", ".."))
_DEPS = os.path.join(_ROOT, "runtime-deps")
_PYLIBS = os.path.join(_DEPS, "python-libs")
_BUNDLED = os.path.join(
_DEPS,
"python-runtime",
"python.exe" if os.name == "nt" else os.path.join("bin", "python3"),
)
def main() -> int:
if len(sys.argv) < 2:
sys.stderr.write("usage: with-deps.py <script.py> [args...]\n")
return 2
# 目标脚本相对 scripts/ 解析(如 office/validate.py也支持绝对路径
arg = sys.argv[1]
target = arg if os.path.isabs(arg) else os.path.join(_HERE, arg)
if not os.path.isfile(target):
sys.stderr.write(f"with-deps.py: target not found: {target}\n")
return 2
# 选解释器:有受控 Python含 lxml且当前不是它 → 用它;否则用当前/系统 Python
interp = sys.executable
if os.path.isfile(_BUNDLED) and os.path.realpath(_BUNDLED) != os.path.realpath(sys.executable):
interp = _BUNDLED
# 注入 defusedxmlos.pathsep 跨平台自动 ';' / ':'
env = dict(os.environ)
if os.path.isdir(_PYLIBS):
existing = env.get("PYTHONPATH")
env["PYTHONPATH"] = _PYLIBS + (os.pathsep + existing if existing else "")
cmd = [interp, target, *sys.argv[2:]]
try:
rc = subprocess.run(cmd, env=env).returncode
except OSError:
rc = 126 # 受控 Python 无法启动
# 受控 Python 跑不了rc<0=被信号杀,如 macOS Gatekeeper126/127=无法执行)
# → 退回系统 Python让脚本在缺 lxml 时优雅降级,而不是把整条命令判为失败
if interp != sys.executable and (rc < 0 or rc in (126, 127)):
rc = subprocess.run([sys.executable, target, *sys.argv[2:]], env=env).returncode
return rc
if __name__ == "__main__":
raise SystemExit(main())