脚本打包与复用(解压即用的便携包)
一句话定位
把"能跑"这件事连同环境一起交出去。 判据只有一条:
> 解压到一台干净机器(干净目录)后,用包内的解释器能跑通。
不满足这条,"我这边测过"就毫无意义。
0. 存放位置 / 前置条件 / 调用方式
存放位置:~/.workbuddy/skills/script-portable-packaging/(用户级 → 所有项目可用)。
无需注册:加载器按目录扫描,目录名必须与 frontmatter 的 name 完全一致(kebab-case)。
若要团队共享,再复制一份到 <项目根>/.workbuddy/skills/(项目级)。
运行环境:
| 资产 | 依赖 | 说明 |
|---|---|---|
scripts/bat_lint.py |
纯标准库(Python 3.9+ 直接跑) | 免 pip install。校验对象是 cmd 语法,只对 Windows .bat 有意义 |
scripts/zip_verify.py |
纯标准库 | 复验 zip 时会执行包内解释器,故需在能运行目标产物的平台上跑 |
references/*.md |
无 | 纯文档 |
调用方式:
SK="$HOME/.workbuddy/skills/script-portable-packaging"
# ① 改完任何 bat 立刻校验(防"双击闪退"的第一道闸,写完就跑)
python "$SK/scripts/bat_lint.py" <项目根 | 单个.bat> --fix-hint
python "$SK/scripts/bat_lint.py" --quiet dist/ # CI:只输出结论行
python "$SK/scripts/bat_lint.py" --selftest # 自证工具本身没坏
# ② 出包后:在干净解压副本上用「包内解释器」复验(★ 不要用手上的虚拟环境)
python "$SK/scripts/zip_verify.py" dist/xxx_便携版.zip \
--smoke "{python} -c \"import yaml, docx, playwright; print('deps ok')\"" \
--run-bat "诊断.bat"
作为 skill 被调用:说"打包成解压即用的压缩包"或"解压后双击闪退"即可命中;
也可显式指定 @skill:script-portable-packaging。
1. 适用场景
该用:
- 用户已经本地跑通,想让不懂技术的人也能用(不能让他装 Python、配 PATH、pip install)
- 目标机器没有外网(公司内网),不能靠"首次运行自动下载"
- 要发给多个人 / 多个部门,环境各不相同
不该用:
- 只有自己用 → 直接跑源码/虚拟环境,别折腾打包
- 对方是开发 → 给
requirements.txt+ 一行命令更省事 - 只需要一个功能 → 优先考虑对方已有工具能做的事(Excel 公式 > 发一个 Python 包)
2. 核心决策:形态怎么选
| 形态 | 什么时候选 | 代价 |
|---|---|---|
| 绿色目录 + 内置运行时(推荐) | 有第三方依赖、有配置文件、要给别人排障 | 体积大(但可接受) |
| 单文件 exe(PyInstaller 等) | 依赖极简、对"一个文件"有硬要求 | 解压慢、杀软误报多、中文路径易炸、日志看不见 |
| 源码 + 安装说明 | 对方是开发 | 不算"免配置" |
判据:只要依赖里带了体积大 + 有原生扩展 + 有数据文件的库
(如 Playwright:自带约 89 MB node 驱动),一律选绿色目录。
做成 exe 会更脆 —— 出问题时你连"它到底解到哪了"都看不到。
额外一条:能被复用的系统组件,不要打进包。
例:走 CDP 接管系统浏览器的方案,不打包浏览器内核,复用目标机已装的 Edge/Chrome ——
省几百 MB,也没有"首次运行要下载内核"这一步。
3. 核心步骤
步骤 1 — 收集运行时与依赖(明确"装什么 / 故意不装什么")
| 装 | 不装 |
|---|---|
完整运行时(sys.base_prefix 整份,或等价的最小集) |
用户数据:work/(含 *-profile 登录 Cookie、草稿、截图 —— 带出去等于把账号送人) |
三方库(sysconfig.get_paths()["purelib"]) |
运行产物:logs/、缓存、__pycache__/*.pyc/*.old |
| 程序本体 + 入口脚本 + 说明文档 | 构建工具:pip/setuptools/wheel(用不到还占体积) |
空的输出目录(work/、logs/ 建好但内容为空) |
可复用的大型系统组件(见上) |
★ 空目录也必须写成 zip 条目(zf.writestr("work/", b"")),
否则解压后它们会消失,程序首次运行还得自己建 —— 有些流程会因为"目录不存在"直接报错。
步骤 2 — 写入口脚本:bat 三条铁律(违反 = 双击闪退)
这是本类任务事故率最高的一处,必须当成硬约束而不是建议。
| # | 铁律 | 为什么 |
|---|---|---|
| 1 | 纯 ASCII | 项目路径通常含中文;bat 里再写中文会控制台花屏、甚至路径解析失败。中文提示一律放进 Python |
| 2 | 全文禁止 ( 和 ) |
★ 闪退根因,见下 |
| 3 | 每条退出路径都先 pause 再 exit /b |
否则一出错就是"窗口一闪而过",用户拿不到任何信息 |
铁律 2 的现场(真实事故):if not exist (%s) ( echo ...(含括号的文字)... )
这样的写法,cmd 解析 if ... ( 块时遇到 echo 文本里的括号会提前闭合这个块,
把剩余文字当命令 → 报 then was unexpected at this time. → 整个 bat 立即中止:
rc=255、一行输出都没有 —— 用户看到的就是"黑窗口闪一下"。
实测对照(留作证据):
| 写法 | rc | 能跑到末尾吗 |
|---|---|---|
echo 文本含括号 |
255 | ✗ / stderr: then was unexpected at this time. |
| 同结构去掉括号 | 0 | ✓ |
→ 完全不用 if (...) 块,一律用 goto 标签组织流程。
入口脚本的其他必备动作:
@echo off
setlocal enableextensions
chcp 65001 >nul 2>nul &rem 铁律配套:编码三件套
cd /d "%~dp0" &rem 双击时工作目录不一定是包根
set "PYTHONUTF8=1"
set "PYTHONIOENCODING=utf-8"
...
rem ---- pre-flight:文件在不在?运行时真能跑吗? ----
if not exist "%PYEXE%" goto ERR_NO_RUNTIME
"%PYEXE%" -V >nul 2>nul &rem ★ 探活:文件在 ≠ 能跑(被杀软锁住/缺 DLL 时一执行就死)
if errorlevel 1 goto ERR_BROKEN_RUNTIME
证据链:入口脚本每次运行都往 logs\last-run.txt 追加 start / end 两行。
这是分辨"跑没跑到脚本"的唯一客观依据:
- 完全没有记录 → 脚本层就被拦了(组策略 / 杀软 / 权限)
- 有 start 没 end → 跑起来后被外部掐断(或自己崩了)
静态校验:scripts/bat_lint.py 把三条铁律 + goto 标签存在性做成可执行检查,
打包时强制跑,不通过直接中止打包;本地开发也随时能跑一遍。
步骤 3 — 生成完整性清单 manifest.json
记录全部文件的相对路径 + 字节数,外加十几个关键文件(缺一个就完全不能用)的 CRC32。
作用:把"解压不完整"从猜变成点名 —— 诊断脚本能一条条列出
"哪个文件少了、哪个文件被截断了"。这是闪退排障的第一手材料。
步骤 4 — 外发前隐私体检
扫 zip 条目,命中敏感模式就先删掉已生成的 zip 再抛异常。
- ★ 命中后必须先删 zip:否则
dist/里留着一个"看起来能发"的包,
下次很可能被人顺手拷走 —— 隐私问题不能靠"我记得刚才报错了" - ★ 敏感模式必须锚定路径(如
work/drafts/、work/images/、work/*-profile/)。
写裸的/images/会把本就该随包分发的样例配图判成敏感,平白打断打包 - ★ 做成可测的纯函数,并双向断言:
① 真敏感的(登录 Cookie、草稿、日志、__pycache__、pip/)必须拦;
② 不该拦的(样例配图、源码、入口脚本、第三方驱动)必须放行。
只测"能拦住"会漏掉假阳性这种反向 bug。
步骤 5 — 在干净解压副本上复验(关键)
自检必须用包内的解释器,不能用手上的虚拟环境。
"能跑"的证据是"解压到干净目录后、用包内解释器能跑"。
复验流程(scripts/zip_verify.py 做了骨架):
- 把 zip 解到临时目录(不要原地解压 —— 原目录里有你的虚拟环境,
一旦"漏跑的依赖恰好从你本机环境被找到",验证就失效了) - 依次跑:导入链检查 →
sys.prefix指向包内 → 配置与外部依赖解析 →
单元测试 → 端到端冒烟 - 真实执行入口脚本(不是"读一遍看内容对不对")
- 校验
manifest.json(文件数、字节数、关键文件 CRC)
步骤 6 — 附一份诊断工具
包里必须有一键诊断入口(诊断.bat → 诊断脚本),它按六类原因逐项点名,
输出 diagnose-report.txt,开头就是【结论】(要动的地方)。
用户闪退时你不用远程连他电脑,只要这份报告。逐项内容见
references/crash-triage.md。
4. 输入与输出
| 内容 | |
|---|---|
| 输入 | ① 已本地跑通的项目目录(含入口脚本、配置)<br>② 运行时类型与版本(如 Python 3.13)<br>③ 需要装的三方库清单<br>④ 关键文件清单(缺一个就不能用的那些)<br>⑤ 敏感路径模式(必须锚定到具体子目录) |
| 中间产物 | 暂存目录(打包用的完整目录树)· manifest.json · 入口 bat 模板 · 诊断报告模板 |
| 输出 | ① <名称>_便携版.zip(解压即用)<br>② 使用说明(目录结构 / 启动方式 / 依赖处理 / 跨设备注意事项 / 完整步骤 / 闪退排障章节)<br>③ manifest.json(完整性清单)<br>④ 诊断.bat + 诊断报告 ⑤ 打包日志(含每项体检的结论) |
使用说明必须包含的 6 节(少一节,用户就会来问你):
- 怎么解压(必须先解压,不能在压缩包里直接双击)
- 解压到哪(不要放
C:\Program Files等受保护目录,也不要放云盘同步目录) - 双击哪个(主入口 / 诊断 / 自检 各是干什么的)
- 需要预装什么(比如"系统要有 Edge 或 Chrome")
- 完整使用步骤(一步到底,不要教他两步)
- 出问题怎么办(跑诊断 → 发报告)
5. 复用方式
5.1 换一个项目打包,只要改四处
- 项目根与名称(产物名、bat 里显示的名字)
- 入口与参数(主入口 bat 调哪个命令、传什么参数)
- 关键文件清单(
CRITICAL_RELS:换成"缺一个就不能跑"的那十几个文件) - 敏感路径模式(锚定到该项目的用户数据目录)
打包逻辑本身不用动 —— 前提是它按"配置驱动"写,而不是把项目名硬编码在各处。
5.2 复用本 skill 的哪些资产
| 资产 | 什么时候用 |
|---|---|
scripts/bat_lint.py |
写/改任何 bat 之后立刻跑。可单文件、可整个目录,支持 --fix-hint |
scripts/zip_verify.py |
步骤 5:解压到干净临时目录 + 用包内解释器跑冒烟 + 校验 manifest |
references/crash-triage.md |
步骤 6 / 用户报闪退时:六类原因逐项排查表 + 诊断脚本要检查什么 |
references/packaging-checklist.md |
出包前后照着走一遍的清单 |
5.3 完整实战案例
docx-to-help-center-upload skill 第 1.2 节记录了本项目在这个流程上的完整落地
(打包脚本、诊断脚本、测试防线、以及那次闪退事故的现场记录)。
本 skill 是它抽象出来的通用层 —— 换项目时参考本 skill 的步骤,具体实现抄那个。
6. 反模式速查
| 反模式 | 后果 | 正解 |
|---|---|---|
| 写安装文档代替打包 | 对方按文档配环境,十步错一步就放弃 | 把运行时装进包 |
| 做成单文件 exe | 解压慢、杀软误报、中文路径炸、日志看不见 | 绿色目录 + 内置运行时 |
| 打包时带上用户数据目录 | 把登录 Cookie 一起送人 | 敏感路径锚定 + 命中后先删 zip 再抛 |
| 用自己机器的解释器验证 | "漏跑的依赖恰好从本机被找到",验证失效 | 只用包内解释器 + 干净解压副本 |
| bat 里写中文/括号 | 花屏 / 双击闪退(rc=255、零输出) | 纯 ASCII + 全文禁括号 + 处处 pause |
| 只做静态检查不执行 | "bat 语法是坏的"能整整漏过一轮 | 验证包装器必须真的执行它 |
| 敏感模式写裸路径 | 假阳性,打断打包流程 | 锚定到具体子目录 + 双向断言 |
| 空目录不写进 zip | 解压后目录消失,首次运行报错 | writestr("dir/", b"") |
| 只测"能拦住" | 假阳性这种反向 bug 无人发现 | 真敏感的必须拦 + 不该拦的必须放行 |
7. 用户报"闪退"时的第一反应
不要先怀疑环境。 按这个顺序:
- 先复现:在自己机器上双击那个 bat(不是调 Python)。
如果自己也闪退 → 是脚本自己的问题,跟对方环境无关 - 看一眼
logs\last-run.txt:完全没有记录 / 有 start 没 end(判据见步骤 2) - 让对方跑诊断脚本,把
diagnose-report.txt发回来 - 逐项排查六类原因(
references/crash-triage.md)
⚠️ 最容易犯的错:用户说"闪退",就顺着"是不是杀软拦截""是不是路径有中文"
一路猜下去,写一堆环境适配 —— 而真正的原因是自己生成的脚本有语法错。
先在自己机器上双击一遍,这一条能省掉 80% 的排查时间。
完整六类原因排查表见 references/crash-triage.md。