打印工作站(公网网页打印)
在已经接好打印机的那台 Mac 上搭一个公网可访问的打印网页:
别人用浏览器上传照片 / 文档 / 票据 → 这台机器排版 → 本地打印机出纸。
本技能带一份可直接跑的完整实现(assets/print-web/),不是纸面流程;
换一台电脑,体检 → 拷目录 → 配两个口令 → 选条公网通道,就能起来。
什么时候用
- 「把打印做成网页 / 让别人也能用我的打印机打印」「发个链接给家长/同事打印」
- 「公网访问本地打印机」「内网打印机给外面用」
- 已经有一台连着打印机的 Mac,要在上面长期跑一个打印服务
先选路:公网怎么开放
使用者的浏览器
│
┌───────────────┼────────────────┐
│ │ │
同一局域网 发链接给外部 临时验证
│ │ │
直接开箱即用 ★ 云端中转(默认) ./tunnel.sh
无需任何配置 妙搭收单 + 本机 cpolar / cloudflare
│ 主动轮询取件 │
│ │ │
└───────────────┴───────────────────┘
▼
本机 :8899 ──排版──▶ lp ──▶ 打印机
| 路径 | 什么时候选 | 要准备什么 |
|---|---|---|
| 局域网直连(默认零配置) | 使用者和你在同一网络 | 无。服务默认监听全部网卡,http://<本机IP>:8899 直接用 |
| ★ 云端中转(默认的公网方案) | 要发给不在同一网络的人 | 建一个妙搭应用(用 miaoda-print-relay 技能),本机跑 pickup_cloud.py |
| 打洞(cpolar / Cloudflare) | 不想建云端应用,或临时验证 | cpolar 账号 token,或 brew 装 cloudflared |
为什么默认走云端中转:不需要公网 IP、不需要端口映射、不装任何隧道客户端,
国内访问快,且隧道被墙/地址随机的问题全绕开了。细节见 references/cloud-relay.md。
别默认 Tailscale —— 在国内网络下它常常连不上控制平面(DNS 被污染成假地址)。
先测可达性再选方案:./assets/print-web/tunnel-doctor.sh。
装机五步
0. 体检
bash scripts/preflight.sh
检查 macOS / CUPS / 打印队列 / Python 依赖 / 公网端点可达性 / 端口占用。
只读,不提交任何打印任务。 有红项先按提示处理,别硬上。
1. 落盘
cp -R assets/print-web ~/print-web # 或任何固定路径
cd ~/print-web
放在固定路径,别放在 OneDrive / 会被清理的临时目录里 —— 数据和日志都在里面。
2. 装依赖 + 演练启动
PY=$(command -v python3) # 或 export PRINTWEB_PYTHON=<绝对路径>
$PY -m pip install fastapi uvicorn python-multipart pillow reportlab pillow-heif
$PY -m pip install playwright && $PY -m playwright install chromium # 要用「文件打印」才装
DRY_RUN=1 ./start.sh # 排版照做,但不真的发打印机
首次运行自动生成 config.json,里面是随机口令和签名密钥:
cat config.json | grep -E 'access_code|admin_code|printer'
printer留空会自动探测系统默认队列;探测不到就手填lpstat -p里看到的名字。- 装
pillow-heif是为了让 iPhone 的 HEIC 直接能读(不装会退到sips,能用但慢)。 - 不想装 playwright 只有照片和发票两种模式可用。
3. 验证三种模式
浏览器打开 http://127.0.0.1:8899,用 access_code 登录,各传一张试试:
| 模式 | 传什么 | 关键选项 | 版式 |
|---|---|---|---|
| 📷 照片 | 图片(多张,iPhone HEIC 也行) | 每页 1 / 2 / 4 张 | 4 张 = 2×2 横向;1 张按中位宽高比自动判横竖 |
| 📄 文件 | PDF / Word / Excel / PPT / 网页 / 文本 / Markdown / CSV / JSON | 无(纵向原样输出) | 多个文件合并成一个 PDF 一次打完 |
| 🧾 发票 | 出租车票、餐饮小票这类细长票据 | 每行 1~4 张、转 0/90/180/270° | 竖向 A4,横排放上半页,下半页留白可手写 |
一定要看预览再提交。 三种模式都会先出预览图(文件模式只预览前 3 页,
超了会给「下载完整 PDF」链接)。
然后只打 1 页真纸验证方向,确认没问题再放开:
# 网页端:先用 1 张图 + 每页 1 张,试打一次
# 命令行也可以直接试:
./send_to_printer.sh ~/print-web/data/jobs/<任务号>/out.pdf auto 1
4. 开放公网 + 装常驻
./install-service.sh # 网页服务装成 launchd,开机自启
# 云端中转:
cp pickup.config.example.json pickup.config.json # 填 base_url / pickup_key
./install-pickup-service.sh
# 打洞(临时):
./tunnel.sh cpolar token <你的authtoken>
./tunnel.sh cpolar start
> ⚠️ install-*.sh 必须在你自己的 Terminal 里跑。被沙箱包裹的自动化环境里
> launchctl bootstrap 会报 Bootstrap failed: 5: Input/output error —— 那是操作不了
> 真实 GUI launchd 域,不是配置写错了。
5. 发口令
| 口令 | 给谁 |
|---|---|
access_code(8 位) |
要用的人 —— 只能上传和打印自己的任务 |
admin_code(12 位) |
只自己留着 —— /admin 管理面板:暂停服务、改限额、看日志 |
改口令:编辑 config.json 后重启服务,不用改代码。
出纸核验:别只信「提交成功」
建单成功 ≠ 打出来了。 这套实现做了四件事来让「成功」可信:
- 分步阶段 ——
queued(已提交)→printing(在打了)→printed(确认出纸)
→failed(失败 + 具体原因:缺纸/卡纸/卡死)。 - 进度感知等待 —— 作业的进度串一变就重置停滞计时器。固定超时会两头都错:
实测 8 页用了 320 秒,越过 300 秒阈值 → 明明出纸了却报失败,用户重打一遍白费纸。 - 出队后再核一次 —— 缺纸时 CUPS 照样把作业从队列里拿走。
只看lpstat -o会把失败当成功。 - 硬故障当场报 —— 等待期间就读
printer-state-reasons,按 severity 后缀分流
(-error才挡出纸,-report只是信息)。用户十几秒就知道该去装纸。
手工核对某次打印:
lpstat -l -p <队列> # 「警报」行 = printer-state-reasons
lpstat -l -o <队列> # 该作业的进度串(如「状态:起始页4」)
安全边界
这是物理设备 + 公网的组合,滥用直接消耗纸和墨。已做的:
| 机制 | 说明 |
|---|---|
| 口令 + HMAC 签名 token | 随机生成、compare_digest 防时序探测、默认 24h 过期 |
| 登录防爆破 | 同 IP 10 分钟错 20 次锁死;但正确口令始终放行(先验口令再判锁定,合法用户不会被连累) |
| 任务归属校验 | 查/打/删都校验 IP 归属,公网下任务号被猜到也拿不到别人的照片 |
| 图片净化 | 原始字节从不落盘,一律 verify + 重编码;80MP 上限防解压炸弹 |
| 尺寸闸门 | 单张 ≤25MB、单次 ≤12 张、单次 ≤5 页(管理面板可调) |
| 页数闸门(云端) | max_file_pages 默认 30 —— 300 页 PDF 不能把一包纸打光 |
| 隐私清理 | 打印完立刻删源图;任务目录按 retention_hours 定时清 |
| 串行打印 | 打印机是物理串行设备,单 worker FIFO,不并发抢 |
| 审计 | 提交/打印/失败/暂停/改限额全写 data/audit.log |
刻意没做:不按人限流、不设每日纸张硬上限。
所以管理面板的「暂停打印服务」是唯一兜住滥用的闸门 —— 口令别外传,
发现异常一键停。
遇到问题
先看坑表:references/pitfalls.md(按现象分组,含成因与修法)。
高频三条:
- 右列照片被裁一半 —— 横向版面没带
orientation-requested。
投递时程序会自动带;真遇到就把config.json的page_geometry改成"rot90"重启
(页面几何 == 媒体几何,任何方向都不会被裁;代价是出纸转 90° 看)。 - 打印机老掉线 / 作业卡死 —— 多半是走了 USB-over-USB。
./switch-print-channel.sh lan <打印机IP>切网络通道(实测 4 页:USB 卡死 7.5 分钟 vs 网络 88 秒)。
设PRINTWEB_PRINTER_SERIAL后脚本会校验身份,防止切错机器。 - 队列被停用、任务不动 —— 装纸后跑
./switch-print-channel.sh fix。
cupsenable之后队列里的作业会自动继续出纸,不要重复提交。
常用命令:
curl --noproxy '*' http://127.0.0.1:8899/healthz # 服务活着吗
tail -f logs/app.log # 服务日志
cd ~/print-web && ./uninstall.sh # 停并卸载
> --noproxy '*' 是必要的:设了 HTTP 代理环境变量的机器上,不加的话连
> 127.0.0.1 会被送到代理上去。
技能包里有什么
print-workstation/
├── SKILL.md
├── scripts/preflight.sh 上机体检(只读)
├── references/
│ ├── pitfalls.md 坑表:按现象分组
│ └── cloud-relay.md 云端中转建站步骤
└── assets/print-web/ 可直接跑的完整实现
├── app.py FastAPI 后端:认证 / 三模式分流 / 打印队列 / 管理 API
├── print_a4.py 照片排版引擎(2×2 / 左右 / 整页)
├── print_invoice.py 发票排版引擎(竖向 A4 + 横排上半页)
├── doc_convert.py 文档 → PDF(textutil / openpyxl / python-pptx / Chromium)
├── pickup_cloud.py 云端取件程序(主动轮询,无需入站)
├── static/ 用户页 + 管理面板(无框架、无构建步骤)
├── tests/test_modes.py 端到端自测(44 项,进程内跑,不碰真实打印机)
├── common.sh 解释器 / 服务名 / 端口解析(改这里就能换环境)
├── start.sh / install-service.sh / uninstall.sh
├── install-pickup-service.sh / tunnel.sh / tunnel-doctor.sh
└── switch-print-channel.sh USB ↔ 网络通道切换
跑自测(不会真的出纸):
cd assets/print-web && python3 -B tests/test_modes.py
可覆盖的环境变量:PRINTWEB_PYTHON(解释器)、PRINTWEB_PORT(端口)、
PRINTWEB_DATA_DIR(数据目录)、PRINTWEB_DRY_RUN=1(演练)、
PRINTWEB_PRINTER / PRINTWEB_PRINTER_SERIAL(队列与序列号)、
PRINTWEB_LABEL / PRINTWEB_PICKUP_LABEL(launchd 服务名)。
相关技能
miaoda-print-relay—— 建云端那个妙搭应用(收单 + 任务表 + 匿名访问)。
本技能的云端中转只管本机这一侧。a4-image-print—— 单机排版打印引擎的完整说明(含 CUPS 方向铁律与实测历史)。
本技能已自带精简版print_a4.py;需要手工排版、钉钉取图、JetDirect 兜底时用它。hp-jetdirect-print—— CUPS 队列真卡死时的最后兜底:绕开 macOS 打印系统,
走 JetDirect 9100 直接发数据。