> 📌 1.5.0(本次):改为「一套源码、两个分发」结构 —— 本包与
> lan-print-macos(macOS 版)共用同一份 print_tool.py,平台差异收敛在内部的
> 平台后端(_win_* / _mac_*)与各包自己的 bootstrap / config.json / 本文档。
> 这样两端不会再各自漂移。
> 本次新增同楼层多台打印机支持(alt_printers,见下文),对既有单打印机配置无影响。
> ⚠️ 本包内的 print_tool.py 与线上 1.2.1 的 Windows 代码路径行为一致,
> 平台分支以外的函数体逐字未改。
Windows 打印(lan-print)
本技能通过同目录的 bootstrap.ps1 操作 Windows 已安装的打印机。Skill ZIP 不包含 EXE 或 CMD;首次运行由脚本下载并校验 Windows x64 运行组件,之后复用本机缓存。无需访问 GitHub、安装 Python 或安装 SumatraPDF。
配置(客户无需手工指定)
目标打印机的信息写在随包 config.json,客户装好即用,不必输任何参数:
| 字段 | 说明 |
|---|---|
printer_name |
目标打印机名 |
printer_ip |
打印机 IP(自动建端口用) |
driver_model |
驱动名,必须与 Windows 驱动库里的显示名完全一致 |
driver_inf_name |
驱动包内真正的 inf 文件名(精确指定,避免选错) |
driver_provider / driver_version |
驱动厂商与版本,仅作记录 |
wifi_ssid |
打印所要求的 WiFi |
print_monochrome_default |
打印是否默认黑白(由启动器注入 --monochrome,见下) |
driver_share_id |
驱动包分享页 id(现解析,见下) |
driver_package_page |
驱动包下载页(失败时展示) |
runtime |
运行组件来源(url / sha256),替换分发渠道时只改这里 |
> ⚠️ 驱动包的外层目录名可能是骗人的。当前 Xerox 驱动包外层目录叫
> UNIV_5.1009.1.0_PS_x64_Driver.inf(一个带 .inf 后缀的目录),
> 而真正的 inf 在里面叫 x3UNIVP.inf。名字对不上,靠猜必错 ——
> 所以 driver_inf_name 是精确指定,不是可选项。
打印机解析顺序:--printer > 环境变量 PRINTER_NAME > config.json > Windows 默认打印机。
config.json 缺失或损坏不会报错,自动降级。
驱动包直链为什么要现解析
分享页给出的 OSS 直链签名有效期仅约 120 秒,固化进分发包必然过期。
所以配置里只存 driver_share_id,安装驱动时现解析,下载重试时也会重新签名。
(实现在 resolve_share_link() / driver_download_url()。)
⚠️ 驱动安装的两个致命坑(换驱动前必读)
坑 1:「装入驱动库」≠「注册进打印子系统」,这是两件事
| 动作 | 实际效果 | Get-PrinterDriver 可见? |
|---|---|---|
pnputil /add-driver |
把 inf 放进 Windows 驱动库(driver store) | ❌ 看不见 |
Add-PrinterDriver |
注册进打印子系统,Add-Printer 才能用它 |
✅ 可见 |
只做前者 → 会出现「pnputil 报告成功,但 Add-Printer 仍找不到驱动」的假成功。
这正是「反复提示装成功的还是旧驱动」的根因。
坑 2:V4 打印驱动不支持 -InfPath
V4 驱动(INF 里 PackageAware=TRUE,如当前用的 Xerox GPD)执行
Add-PrinterDriver -InfPath 会报「为此操作指定的一个或多个参数值无效」——
这是预期行为,不是失败。若把它当第一步,就会被误判为失败并退回 pnputil,
从而掉进坑 1。
正确顺序(print_tool.py 与 lan-print-install.ps1 均已按此实现):
① Get-PrinterDriver 按名查 → 已在打印子系统 ? 直接过
② Add-PrinterDriver -Name "<驱动名>" → 成功即完成
③ pnputil /add-driver <inf> /install
然后**必须**再 Add-PrinterDriver -Name → 装库后补注册,不可省
④ Add-PrinterDriver -Name -InfPath <inf> → 仅 V3 驱动有效,最后的兜底
坑 3:端口协议会被旧驱动遗留污染
换品牌后旧的 LPR/515 端口会留在系统里。Xerox 新机走 LPR 可能出现
「任务发出但不打纸」,所以端口不仅要「存在」,Protocol 还要是 RAW、
PortNumber 要是 printer_port(9100)。不匹配时脚本会自动重建端口。
坑 4:Remove-Printer 不能吞错
删除失败时 Add-Printer 会报「指定的打印机已存在」——一个完全看不出根因的错。
所以删除后必须复查对象是否真的消失,没删掉就明确报 PRINTER_REMOVE_FAILED
并提示手工移除,而不是硬着头皮往下走。
调用
先确定本技能目录 <skill_dir>,通过 Windows PowerShell 调用:
bootstrap.ps1 --json <子命令> [参数]
--json 必须放在子命令前。依据进程退出码判断成功或失败;不要用中文关键词猜测结果。
> ### ⚠️ WorkBuddy 宿主里怎么起这个脚本(2026-09-22 实测,务必先读)
>
> 不要从 Bash 里调 powershell.exe —— WorkBuddy 会直接拦截,报
> 「Invoking PowerShell from Bash bypasses PowerShell security checks; use the PowerShell
> tool instead」,命令根本不会执行。而且拦截是按字符串匹配的:哪怕只是
> ls ... && grep ... powershell.exe ... 这种顺带提到,也一样被拒。
>
> 正确做法:用 PowerShell 工具执行,工具内部就是 PowerShell,不需要再套一层 powershell.exe:
>
> powershell
> & "<skill_dir>\bootstrap.ps1" --json --out-file "<export_dir>\r.json" <子命令> [参数]
>
>
> 另一个实测坑:PowerShell 工具的 stdout 也拿不到(本次 Get-Content ... | 输出 只回了一句
> Command completed with exit code 0,没有正文)。所以——
>
> - --out-file 是这个宿主里唯一可靠的取数通道,每次都带上,别只看 stdout。
> - 脚本跑完后用 Read 工具读那个 JSON 文件(不要依赖终端回显,也不要用
> Get-Content -Raw 指望它打在 stdout 上)。
> - 多个文件连续打印时,可以一次 PowerShell 调用串行执行多条 & $b ... print,
> 每条写各自的 --out-file,再逐个 Read 回来 —— 串行提交也顺带保证了打印顺序。
> - 退出码仍然可信;Command completed with exit code 0 就代表这一步成功。
取回结果:--json + --out-file
powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "<skill_dir>\bootstrap.ps1" --json --out-file "C:\temp\result.json" <子命令> [参数]
--json 与 --out-file 都放在子命令前。
为什么需要 --out-file:并不是所有宿主都能可靠捕获子进程的 stdout ——
IDE 内置终端、沙箱环境常常拿不到任何输出(本项目开发过程中实测踩到过)。
落盘回传不经过管道,是更稳的取数方式,建议自动化调用一律带上。
| 约定 | 说明 |
|---|---|
| 落盘内容 | 固定为 JSON(UTF-8),与是否传 --json 无关 —— 这条通道是给机器读的 |
| 失败也写 | 成功与失败都会落盘。拿不到 stdout 时,这是唯一能知道失败在哪一步的途径 |
| 与退出码一致 | 文件里的 ok 字段与进程退出码含义相同,两者都可信 |
| 写失败不改退出码 | 主操作可能已成功,不会因回传通道故障而误报失败;此时 stderr 会给出告警,回退读 stdout 即可 |
调用后先读该文件再判断结果;若文件不存在,说明写入失败(看 stderr 告警),
此时回退用 stdout。
首次运行要求:
- Windows x64。
- 联网(首次运行需下载运行组件)。
- 运行组件缓存于
%LOCALAPPDATA%\WorkBuddy\lan-print\runtime-v1\;每次运行都会校验 SHA-256。
(目录名runtime-v1是缓存槽位标识,与 exe 版本号解耦 —— 升级运行组件时只改config.json的
runtime.file_name/url/sha256,缓存目录保持不变,老用户无需清理旧目录。) - 是否需要
wecom-cli取决于分发渠道(脚本按 URL 自动识别):
| 渠道 | 地址特征 | 额外依赖 |
|---|---|---|
| wsdbrand 分享页(当前默认) |share.wsdbrand.com| 无需任何依赖 |
| 企业微信微盘 |drive.weixin.qq.com| 需wecom-cli≥ 1.1.0 且已完成授权 |
| 普通 https 直链 | 其他 | 无需任何依赖 | - wsdbrand 渠道下发的是 zip 包裹(内层才是真正的 exe/SumatraPDF),启动器会
自动解压并校验内层文件,无需人工干预。 - 更换分发渠道只需改
config.json的runtime.url,启动器无需改动。
子命令
list
status [--printer "打印机名"]
doctor [--printer "打印机名"] [--auto]
install-driver [--printer "打印机名"]
discover [--subnet "10.20.0.0/24"] [--timeout 1.2]
print --file "文件路径" [--printer "打印机名"] [--copies 1..20] [--monochrome] [--color --confirm-color] [--fit] [--dry-run] [--no-precheck]
cancel [--printer "打印机名"] (--job 任务ID | --all)
discover:换设备后定位打印机
打印机 IP 变了、或不确定设备在哪时用它:并发扫描配置 IP 所在的 /24 网段,
探测 9100(RAW) / 631(IPP) / 515(LPD) 三个打印端口,列出候选设备并标出
当前配置的那个。只做被动端口探测,不发任何打印数据,对设备无副作用。
discover [--subnet 10.20.0.0/24] [--timeout 1.2]
结果里如果当前配置的 IP 不在列表中,说明设备换了地址,把新 IP 填进
config.json 的 printer_ip 即可。
⚠️ 但「找到几个 IP」≠「知道哪台是哪台」
discover 只认端口、不认型号。2026-09-21 实测 17F 网段里同时躺着 4 台打印设备
(2 台 HP LaserJet、1 台 Fuji Xerox、1 台 Xerox AltaLink),端口扫描无法区分
哪台是目标机——猜错就会把文件打到别的机器上(还是别人的部门)。
补一刀只读 SNMP 查询即可拿到厂商 / 型号 / 序列号(标准 MIB,团体名 public,
只读、无副作用,任何 snmpget 工具都行):
| OID | 含义 |
|---|---|
1.3.6.1.2.1.1.1.0 |
sysDescr(厂商 + 型号 + 固件) |
1.3.6.1.2.1.1.5.0 |
sysName(设备主机名) |
1.3.6.1.2.1.43.5.1.1.16.1 |
prtGeneralPrinterName |
1.3.6.1.2.1.43.5.1.1.17.1 |
prtGeneralSerialNumber |
实测样例:
10.20.0.243 FUJI XEROX ApeosPort-V C3375 T2 彩色 SN TC100881475085 ← 17F 打印室目标机
10.20.0.244 Xerox AltaLink B8155 黑白 SN HQH261673
10.20.0.240 HP LaserJet MFP M426fdw 位置标注 EPL TRAINING CENTER
10.20.0.247 HP LaserJet MFP M426fdn
💡 型号命名可当快速判据,能省掉一半的 SNMP 查询:
Xerox / Fuji Xerox 的 C=彩色、B=黑白(故 AltaLink B8155 是黑白机,
ApeosC3375 / ApeosPort-V C3375 是彩色机);HP 的 dw/fdw 是无线款、dn/fdn 是有线款。
⚠️ 顺带影响打印行为:本 skill 的默认机是黑白机(.244,硬件不支持彩色),
所以默认输出必然是黑白;要彩色须显式指向彩色机 .243,并走
--color --confirm-color 的二次确认(见「色彩策略」一节)。
⚠️ prtGeneralPrinterName 是设备端名称,不等于 Windows 本机打印机名
(「17F打印机」是本机自定义的),所以它不能反查目标机 —— 型号只能缩小范围,
最终仍需人来确认(看机身标签 / 问使用人)。确认后再走下面的端口重指流程。
install-driver:已存在打印机时会自动校正
⚠️ 这一点很关键。旧行为是「打印机存在就直接返回已安装」,导致
「驱动失效 → 换新驱动包」时新驱动永远装不上。现在的行为:
| 情况 | 动作 |
|---|---|
| 打印机不存在 | 正常创建 |
| 已存在且驱动名一致 | 直接返回,不做任何改动 |
| 已存在但驱动名不一致 | 先 Set-Printer 无损换驱动;失败才 Remove-Printer 后重建 |
删除重建会在输出里明确标注,不静默删东西。
⚠️ 换 IP 后,端口不会自动跟着变(2026-09-21 实测踩坑)
这是个静默失效的坑:上表「已存在且驱动名一致 → 直接返回」这条捷径,
在 print_tool.py 里是提前 return,跳过了后面的端口校正逻辑。
于是打印机 IP 变了之后重跑 install-driver,只会回一句
「已安装且驱动一致,无需重复安装」,而打印机仍指向旧 IP —— 看着装好了,实际打不出纸。
所以改完 config.json 的 printer_ip 后,必须另做一次端口重指(无损,不删打印机):
Add-PrinterPort -Name "<新IP>" -PrinterHostAddress "<新IP>" -PortNumber 9100
Set-Printer -Name "<打印机名>" -PortName "<新IP>"
做完务必再跑一次 doctor,确认第 3 层变成「可达」。
旧 IP 的端口会作为残留保留(无害);要清可用 Remove-PrinterPort。
装驱动本身走三层回退,适配 V4 打印驱动(当前 Xerox 包就是 V4 / PackageAware=TRUE):
Add-PrinterDriver -Name <名> -InfPath <inf>← V4 驱动首选pnputil /add-driver <inf> /install← 老驱动包 / 旧系统Add-PrinterDriver -Name <名>← 驱动已在库中时按名注册
- 打印机解析顺序:
--printer、环境变量PRINTER_NAME、config.json、Windows 默认打印机。 --fit只把超过可打印区域的页面缩小到纸张内,不会把多页文档合并成一页。--dry-run只验证文件、格式和打印机,不提交任务。print默认先做前三层预检(驱动 → WiFi → 连通性)。不合格直接中止并说明断在哪层,不会让任务悄悄卡在队列里。--no-precheck可跳过。- 支持 PDF、png/jpg/jpeg/tif/tiff/bmp、docx、xlsx/xls。
- Word/Excel 优先使用本机 Microsoft Office 转换为 PDF;没有 Office 时尝试 LibreOffice。
色彩策略:默认黑白,彩色需二次确认
两条规则,缺一不可:
| 规则 | 实现 |
|---|---|
| 默认走黑白,且默认机就是硬件黑白机 | config.json 的 printer_name = 17F黑白打印机(.244,设备本身不支持彩色) |
| 彩色必须二次确认 | 启动器要求 --color 与 --confirm-color 同时出现 |
调用方(Agent)该怎么做
- 用户没提配色 → 直接打印,不要追问。默认就是黑白,且走的是硬件黑白机。
- 用户明确要彩色("打彩色的"、"这个要彩印")→ 不要立刻提交,先复述确认一次
(例如「确认要打彩色吗?彩色耗材更贵」),拿到肯定答复后才提交:
print --file "..." --printer 17F打印机 --color --confirm-color
彩色只能走 17F打印机(.243,唯一支持彩色的那台)。
3. 只传 --color 不传 --confirm-color → 启动器以 COLOR_NEEDS_CONFIRMATION
(退出码 46)拒绝执行并给出提示。这是刻意的:只认一个参数挡不住两类误判
——「把用户的模糊表述当成要彩色」和「顺手加 --color」,而那会静默出彩页。
| 想要的效果 | 怎么调 |
|---|---|
| 默认黑白(当前) | 不传任何配色参数 |
| 打彩色 | --color --confirm-color,且 --printer 17F打印机 |
| 临时改默认 | 环境变量 PRINTER_MONOCHROME=0(同样会被二次确认拦住) |
| 永久改默认 | config.json 的 print_monochrome_default 改成 false(同样要确认) |
为什么这个开关做在启动器(bootstrap.ps1)而不是后端?
后端是远程预编译的 exe(runtime.url 指向的包,带 SHA-256 校验),
改它的源码不会生效。而启动器负责拼参数 —— 在这里补 --monochrome、加色彩闸门即可,
无需重新构建/发布运行组件,也不需要改 print_tool.py。
优先级:环境变量 PRINTER_MONOCHROME > config.json > 默认 false。
--color 为什么必须由启动器「剥掉」而不是传给后端?
因为后端(SumatraPDF)的 -print-settings 只认 monochrome,没有 color token
(已实测其二进制内的取值表)。也就是说一旦注入了黑白,就没有任何办法在同一队列上
强制彩色。所以启动器把 --color / --confirm-color 一起吃掉(不让后端看到,
否则会报未识别参数),用「不注入」来得到彩色。这也意味着一件事:如果同时在
Windows 驱动层把该打印机的默认配色设为单色(见下),--color 就会失效。
> ⚠️ 默认机是硬件黑白机,所以哪怕闸门被绕过、往默认队列提交了彩色任务,
> 出来的仍然是黑白页 —— 这是刻意的第二层保险。
⚠️ 「默认黑白」的两个层次,别混
| 层次 | 手段 | 影响范围 | 彩色出口 |
|---|---|---|---|
| 本 skill(已启用) | print_monochrome_default → 启动器注入 --monochrome + 色彩闸门 |
只影响经本 skill 提交的任务 | ✅ --color --confirm-color |
| Windows 驱动默认 | Set-PrintConfiguration -PrinterName "17F黑白打印机" -Color $false |
本机所有程序打这台机器都变黑白(含 Word、浏览器) | ❌ 只能改回来 |
驱动层那条需管理员权限,且会吃掉 --color(Sumatra 无 color token)。
默认机本身就是黑白机,没必要动它。查看当前驱动默认配色:
Get-PrintConfiguration -PrinterName "17F黑白打印机" # 关注 Color 字段
doctor:四层环境体检
打印失败时,最怕「不知道断在哪」。doctor 从近到远逐层排查:
| 层 | 检查内容 | 失败含义 |
|---|---|---|
| 1 | 驱动与打印机安装 | 本机没装这台打印机,或装着的驱动与配置要求的不一致 → 需 install-driver |
| 2 | WiFi 网络环境 | 没连到要求的 wsdsocial |
| 3 | 打印机连通性 | TCP 探测打印机 IP(默认 9100,回退 515/631/445),失败再 ping 区分「不可达」与「端口被拦」 |
| 4 | 打印机状态 | 缺纸/卡纸/墨粉/脱机等 |
第 1 层会同时校验驱动名:打印机对象在、但驱动不是 config.json 里 driver_model
指定的那个,同样判 FAIL(这是「换了驱动包但没生效」的典型症状 —— 只看打印机在不在会漏掉它)。
--auto 会在第 1 层失败时自动安装/切换驱动(需管理员权限)。
第 2 层不会自动切换网络——只提示并停下,等用户确认(切网会短暂中断本机网络)。
data.fix_instructions 里给出每层的修复建议,可直接转述给用户。
给不用 WorkBuddy 的同事:一键安装脚本
SkillHub 技能包不允许 .bat 文件(平台安全策略),所以一键安装脚本单独分发:
https://share.wsdbrand.com/doc/viewer?id=R52p8WYJItzOf5Oj
下载解压后得到三个文件,双击 bat 即可完成「下载驱动 → 装驱动 → 建端口 →
建打印机 → 设默认」全流程,不需要 Python、不需要 WorkBuddy:
一键安装打印机驱动.bat ← 双击这个
lan-print-install.ps1 ← 必须与 bat 同目录(bat 会调用它)
使用说明.txt ← 给人看的,非必需
前两个缺一不可(第三个只是说明)。之所以不做成单个 bat:bat 单行长度上限 8191 字符,
装不下完整逻辑,硬塞会把脚本变成不可维护的拼接。
bat 会自动请求管理员权限(UAC 弹窗点「是」)。参数与 config.json 一致,
换驱动或换 IP 时两处都要改:
| 变量 | 应为 |
|---|---|
$PrinterName |
17F打印机 |
$PrinterIp |
10.20.0.243 |
$DriverModel |
Xerox Global Print Driver PS |
$DriverInfName |
x3UNIVP.inf |
$ShareId |
si7P5ObInbUGUx5D |
> ✅ 2026-09-22 已修复并换新条目:$PrinterIp 已改为现网可达的 10.20.0.243,
> 重新发布为 OA 条目 lan-print-oneclick-install-v3(即本页上方那个链接),并做了下载回验:
> 线上文件 SHA-256 与本地构建产物字节级一致、包内 ps1 确认写的是 10.20.0.243。
> 此前两个旧条目 —— lan-print-oneclick-install(包内 ps1 仅 9,599 字节那版)与
> lan-print-oneclick-install-v2 —— 经实测确实写死 10.20.0.246,
> 不要再分发,建议在 OA 里删除以免混淆。
>
> 📌 改 IP / 换驱动时的分发纪律:ps1 里的 $PrinterIp 与 config.json 是两处独立副本,
> 必须同时改;且 OA 不支持覆盖已发布文件,只能新建条目。
> ⚠️ 编码约束:bat 用 GBK 存中文(cmd 默认代码页 936),ps1 用 UTF-8 带 BOM
> 存(PowerShell 5.1 读无 BOM 的 UTF-8 会按 GBK 解析,中文注释会破坏语法)。
> 改动这两个文件时务必保持编码,否则会出现「乱码」或「脚本静默退出」。
🔴 已知坑:xlsx 打印「内容不完整」(2026-09-22 定位并修复)
症状:print --file xxx.xlsx 报成功、队列也清空,但纸张上内容只出来一部分
(右侧/下方被切掉),看着像「没打全」。
根因:本机 Excel + Xerox GPD PS 驱动下,ExportAsFixedFormat 在页面缩放 < 100%
时,写出的 PDF 页面尺寸 = 纸张 ÷ 缩放比例:
| PageSetup.Zoom | 导出 PDF 的 MediaBox | 说明 |
|---|---|---|
| 100 | 842.04 × 594.96 pt | ✅ 真 A4 横版,唯一正确的一档 |
| 90 | 935.60 × 661.07 pt | = A4 ÷ 0.9 |
| 57(fit-to-page 自动算出的) | 1477.26 × 1043.79 pt | = A4 ÷ 0.57 |
也就是说:本该「把内容缩到 A4 里」,实际变成「把纸放大了」。页面被撑到 521×368 mm
后,打印机侧按 100% 输出,超出物理 A4 的部分静默裁掉 —— 这就是内容缺失。
PrintScale = shrink 兜不住,因为驱动上报的可打印区域同样是那个被放大的尺寸,判定为「无需缩小」。
排查要点(已证伪的假设,别重复走):
- 不是台账文件的问题:新建空白工作簿 + Zoom=90 同样复现。
- 不是内嵌 printerSettings1.bin:删掉它、删掉 pageSetup 的 r:id、补 fitToWidth/Height,导出尺寸一模一样。
- 不是驱动谎报纸张:PrinterSettings.PaperSizes 里 A4 就是 210×297 mm。
- 不是 --fit 的锅:--fit 只加 Sumatra 的 shrink,改不了 PDF 自身页面尺寸。
- 唯一变量是缩放档位:这是目前已知唯一的触发条件。
判定与处置:导出后检查 PDF 的 MediaBox。
- 若宽 ≈ 842(A4 横版)→ 正常,直接打。
- 若宽明显 > 842(A4 比例)→ 必须归一化:把页面连同内容整体缩 842.04 / 页面宽,
正好等于 Excel 内部那个缩放比例(1477.26 → 842.04 即 0.5699)。
已实现为 print_tool.py 的 normalize_pdf_page_size(),在 convert_xlsx_with_excel()
导出后自动调用(非 A4 比例如 A3/Letter 不擅自改;已是 A4 则原样放行)。
> ✅ 已修复并上线(2026-09-22):运行组件已重建为 v5,config.json 的
> runtime.file_name/url/sha256 已指向 v5,构建环境已加 pypdf(用到
> PageObject.add_transformation)。新装或已装的用户下次运行会自动拉取 v5。
>
> ⚠️ 为什么必须重建 exe:bootstrap.ps1 跑的是打包好的
> lan-print-runtime-windows-x64-vN.exe(不是本目录的 print_tool.py)——
> 只改源码不同步 exe,客户端行为完全不变。
>
> ⚠️ 陷阱:归一化失败是静默的。normalize_pdf_page_size() 在 import pypdf
> 失败时只返回一段告警、不抛异常(设计如此:不让 pypdf 缺失阻断打印)。
> 因此如果 pypdf 没被打进 exe,现象是悄悄跳过归一化、原 bug 照旧复现,极难发现。
> 重建后必须实测冻结环境下 pypdf 可用(v5 已用同配置探针 exe 实测:
> pypdf_import = OK,1477×1044 pt → 841.89×595.28 pt)。
>
> 旧版(v4 及更早)用户的应急做法:先在 Excel 里导出 PDF(会得到同样被撑大的页面),
> 按 842.04 / 页面宽 把页面连同内容缩回真 A4,再把修正后的 PDF 交给 print
> (PDF 走原样打印、不做任何转换,因此不会再被撑大)。只导出、不归一化就交给
> print,等于把问题原样复现一遍。
🔴 已知坑:宿主「安全删除」包装 + StrictMode = 下载装不上(2026-09-22 定位并修复)
症状:首次运行(或换版本后)需要下载运行组件时,下载明明完成、却报装不上;
stdout / --out-file 的 JSON 全是空的,只在 stderr 里看到一句莫名其妙:
Remove-Item: <skill_dir>\bootstrap.ps1:33x
| Remove-Item -LiteralPath $extractDir -Recurse -Force -ErrorAc …
| 无法检索变量 '$LASTEXITCODE',因为尚未设置该变量。
根因:宿主环境(WorkBuddy / CodeBuddy)会把 Remove-Item 替换成一个全局函数
「安全删除包装」(内部引用 $env:CODEBUDDY_SAFE_DELETE_TRASH_BIN /
$env:GENIE_TRASH_DIR,把目标送回收站而不是真删)。该包装内部有一句
$exitCode = $LASTEXITCODE;而 bootstrap.ps1 开了 Set-StrictMode -Version Latest,
在还没跑过任何原生程序时 $LASTEXITCODE 是「未定义变量」→ 读取即抛
VariableIsUndefined → 整个 Remove-Item 失败 → 解压临时目录清理崩掉 → 安装中止。
它是怎么被触发的:Install-Asset 发现缓存里没有目标版本 → 走下载分支 →
Expand-DownloadedPackage 的 finally 里 Remove-Item $extractDir 撞上包装。
(缓存已命中时 Install-Asset 直接 return,不碰这条路径 —— 所以「以前能跑」不代表没问题,
只有真正发生版本升级时才暴露。)
已做的两处修复(都在 bootstrap.ps1):
| # | 修复 | 说明 |
|---|---|---|
| 1 | 紧接 Set-StrictMode 之后加 $global:LASTEXITCODE = 0 |
让任何宿主包装读 $LASTEXITCODE 都能拿到确定值(该脚本尾部原本就为同一原因做过一次,现在提前到最前) |
| 2 | 新增 Remove-ScratchPath(),用 [System.IO.Directory/File]::Delete 替换全部 3 处 Remove-Item |
① 不做命令解析,天然绕开宿主包装;② 我们的 extract-* / pkg-* 是纯中间产物,不该被送进回收站;③ 宿主包装收到多个路径时会拼成一个字符串再 Resolve-Path → PathNotFound |
排查要点(供后续复用):
- 症状像「网络/分发渠道坏了」,实际是本地宿主包装与脚本 StrictMode 的组合问题,
换渠道、换链接都无济于事。
- 拿不到 stdout 时用包装脚本 + 打印 $Error 栈定位:$Error[0].ScriptStackTrace
会直接点出被劫持的 global:Remove-Item 与内部行号 —— 比看日志快得多。
- 判断宿主是否劫持了命令:Get-Command Remove-Item -All 若第一行 CommandType=Function,即为被接管。
- 清理失败会留下实打实的垃圾:每失败一次残留一个 22 MB .download + 一个
extract-* 目录 + 一个 pkg-*.exe。修好后务必去
%LOCALAPPDATA%\WorkBuddy\lan-print\runtime-v1\ 手工清一次。
> 注:一键安装脚本 lan-print-install.ps1 没有开 StrictMode,且同事是双击 bat
> 在普通 PowerShell 里跑(没有宿主包装),因此不受此问题影响,无需修改。
操作规则
> ### 🚧 硬门禁:任何打印请求的第一步必须是 doctor
>
> 任何打印请求的第一件事,必须先运行 doctor 确认四层环境;首轮对话不跳过此步。
>
> - 顺序固定:先 doctor → 后 print。不允许直接跳到 print。
> - 四层中有任何一层 FAIL,先停下来把断点告诉用户并给修复建议,
> 不要提交打印任务(print 自身的前三层预检也会拦截,但那只是兜底,不能替代 doctor)。
> - 不得以下列理由跳过:用户催得急、文件很简单、刚才刚打印成功过、
> 或者「上次已经体检过了」。环境(网络 / 打印机状态)随时可能变。
> - 唯一的例外:用户明确表示只要体检结果、不要打印 —— 那 doctor 本身就是终点。
>
> 这条门禁的意义在于:打印任务一旦提交到队列,失败往往是静默的
> (任务卡在队列里、纸张没出来),事后排查成本远高于事先 10 秒的体检。
- 用户明确指定文件、打印机和份数时,可直接打印;未指定打印机时使用配置里的目标,并在结果中告知实际目标。
- 打印默认黑白(
print_monochrome_default: true+ 默认机是硬件黑白机)。用户没提配色
就直接打,不要追问;用户明确要彩色时,必须先复述确认一次,得到肯定答复后再用
--printer 17F打印机 --color --confirm-color—— 只给--color会被启动器以
COLOR_NEEDS_CONFIRMATION(退出码 46)拦下,别绕、也别去改config.json。 - 提交后用
--dry-run的返回或结果里的monochrome字段可以确认本次是不是黑白。 - 用户只是询问“怎么打印”或表达意图不明确时,不要提交任务,可先使用
--dry-run。 - 取消单个任务前可先运行
status获取任务 ID。 - “取消任务”不能推断为取消全部;只有用户明确要求清空该打印机队列时才使用
cancel --all。 --copies最大为 20。不要通过循环多次调用来绕过限制。- 成功只表示任务已提交到 Windows 打印队列,不代表纸张已经输出;离线、缺纸或卡纸可能在提交后才出现。
- 安装驱动需要管理员权限。非管理员时脚本会明确说明并给替代路径,不会假装成功。若驱动包只有交互式
setup.exe(无.inf),会识别并提示手工双击。
输出
- 退出码
0:成功,JSON 输出到 stdout。 - 非零退出码:失败,JSON 错误输出到 stderr。
- 向用户转述
message;需要进一步操作时,可读取data中的打印机名、任务 ID 和状态。 - 常见错误码:
PRECHECK_DRIVER_FAILED(70) /PRECHECK_WIFI_FAILED(71) /PRECHECK_UNREACHABLE(72) 表示打印前预检未过;DRIVER_ADMIN_REQUIRED(63) 表示需提权;DRIVER_INSTALL_FAILED(66) 多为driver_model与驱动库显示名不一致;SUBNET_INVALID(68) 是discover --subnet传了非法网段。 WECOM_CLI_*、WECOM_AUTH_REQUIRED或DOWNLOAD_*表示首次下载条件未满足;修复后直接重试即可。