我写了个 Windows 脚本管理器:装脚本、自动配环境,相当于桌面版油猴
写脚本的人大概都遇到过这个场景:脚本发出去,对方先问 Python 装哪个版本,再问 pip 报错怎么办,最后问命令行在哪打开。
前两个还能答,第三个很难解释。对写代码的人来说,「打开终端」不算一个步骤;对不写代码的人来说,那是一个入口,而且是没有门把手的那种。
脚本本身没问题。问题在分发。
浏览器那边早有答案
如果你用过油猴(Tampermonkey)或者 ScriptCat,会觉得这事很简单:装个扩展,点一下安装脚本,脚本就开始工作。用户不需要知道 JavaScript 引擎在哪,也不需要知道脚本怎么被注入页面。
我在做的这个东西叫「不忙脚本盒子」,品类上说,是一个 Windows 脚本管理器。浏览器里那一类工具通常叫用户脚本管理器,桌面这边基本是空白。
为什么不走插件 API 路线
做这类工具有两条路。
第一条是统一插件 API:定义一套接口,插件按这套接口写,平台负责调用。好处是可控,坏处是能力上限由 API 决定,你要用一个已经存在的 Python 项目,得先把它重写成插件。
第二条是直接跑脚本:不规定怎么写,只规定怎么声明。脚本还是原来的脚本,语言还是原来的语言,平台负责装环境、传参数、收结果。
我选了第二条。理由是桌面上的需求太散:有人要处理 Excel,有人要转视频,有人要 OCR,有人要定时备份。这些需求背后都有成熟的开源项目,但它们的语言、依赖、运行方式各不相同。如果要求每个项目都改写成插件,等于把生态自己砍掉一半。
代价也很明显:我得接管运行时。
接入协议:一份 TOML 声明
油猴脚本顶部有一段 metadata,写明名字、版本、匹配哪些网址。盒子对应的是脚本目录下的一份 bm-scripts-box-rc.toml:
[bmscriptsbox.info]
name = "PDF 批量压缩"
version = "1.1.0"
desc = "把 PDF 压到指定体积以内"
[bmscriptsbox.runtime]
language = "python"
language_version = ">=3.8"
entry = "main.py"
binaries = [{ name = "pdfcpu" }]
[bmscriptsbox.triggers.context_menu]
enabled = true
targets = ["files"]
filters = [".pdf"]
[[bmscriptsbox.inputs]]
name = "target_paths"
label = "PDF 文件"
type = "list"
exts = [".pdf"]
pick = "files"
几个设计取舍:
不用 SDK,不用 import,不用继承基类。 盒子启动脚本时,把参数写成一个 JSON 文件,路径作为命令行第一个参数传进去。脚本读它、干活、退出。现有脚本接进来,基本不用改代码。
用 TOML 而不是塞在脚本头部的注释里。 注释式的元数据只适合单文件脚本,桌面脚本经常是「入口文件 + 若干模块 + 资源目录」,配置独立成文件更好维护,也能写注释和分层。
触发方式是声明出来的,不是猜出来的。 一个脚本能接受文件、能接受文本、能定时跑,都由声明决定,盒子据此决定在哪些入口里出现。
运行时:最脏的那部分活
浏览器不用管运行时,浏览器就是运行时。桌面没有这个前提,所以安装脚本的时候,盒子要做完这些事:
- 识别并安装对应语言的运行时(Python / Node 等);
- 装 pip / npm 依赖;
- 下载外部二进制,比如 ffmpeg、pandoc;
- 每个脚本一套独立环境,避免版本互相打架。
做过类似事情的人大概能猜到坑在哪:便携运行时和系统已装版本冲突、PATH 被污染、依赖装到一半失败留下半个环境、中文路径和编码问题、以及被杀软误判。
这些都不算优雅的技术问题,但它们是「用户能不能用上」的实际门槛。这一层花的时间,通常比功能本身更多。
有一个边界必须说清楚:依赖隔离解决的是版本冲突,不是安全隔离。 每个脚本仍然以当前用户权限运行,盒子不提供沙箱。这个限制在选型时就需要知道,不能等到用户问起来才解释。
界面:脚本写声明,宿主渲染
油猴脚本的界面写在网页上,天生有地方放。桌面脚本没有页面可以寄生,界面只能由宿主生成。
所以盒子把参数声明变成窗口:声明了文件列表,就渲染成选择框;声明了枚举,就渲染成下拉;声明了布尔值,就有了开关。原来只有黑框命令行的脚本,启动之后变成一张能填的表。
这在实现上比听起来麻烦。参数类型不够用是常态:一个字段既要能填文本,又要能选文件,还要能拖进来一批路径;校验规则经常是「扩展名对、但内容不对」。我最后把参数模型做成可扩展的类型,而不是固定几种控件。
下图GIF的脚本窗口就是渲染出来的
触发方式:桌面版的 @match
@match 决定脚本什么时候生效,答案是 URL 匹配上的时候。
桌面脚本没有 URL,答案变成了系统事件:
- 选中文件后右键,路径自动带进去;
- 给常用脚本绑一个全局热键;
- 选中一段文字,连按两次
Ctrl+C唤起面板,文本作为参数; - 设成定时任务,每天几点跑一次。
这四种入口覆盖了大部分桌面场景,剩下的交给面板里点卡片启动。
脚本社区:管理器真正的内容来源
脚本管理器如果没有内容,就只是个空壳。所以社区不是附属功能,是这个项目的一半。
盒子内置脚本社区:分类浏览、关键词搜索、安装量作为参考,点一下就装。安装的同时会把运行时和依赖准备好,不用回到命令行补步骤。
更新链路是自动的。作者提交 GitHub 仓库地址即可发布,配好 Webhook 之后,push 一次代码,社区版本自动同步,用户端收到升级提示,一键更新,不用卸载重装。
发布一个脚本只有三步:写声明、本地打包自测、提交仓库地址。没有审核排队,也没有私有格式。
社区现在有近百个工具,覆盖文件整理、图片压缩、格式转换、表格文档、OCR、音视频。数量不算多,所以又开了需求中心:用户提想要的功能,能做的我来做,会写的可以认领。
这些脚本由各自的作者维护,盒子只做检索和安装。装之前能看到源码,觉得不对就不装,这比「官方精选」更实在。
开源和生态,是同一件事
「开源」这个词被用得太泛,先把范围说清楚,再说生态。
盒子本体在 GitHub 上公开,用的是 MIT + Commons Clause
脚本那部分是另一套结构:盒子不托管任何脚本代码,只提供检索和安装。来源有三种,本地 ZIP、GitHub 仓库、脚本社区,装到本地仍是源码形态,不是编译产物。所以你能看到脚本实际做了什么,作者哪天不维护了,也能自己接着改。
在这个前提下,生态开放才有意义。它在项目里是三件事:
语言开放。 支持 Python、Node.js、PowerShell、BAT、AutoHotkey、HTML 和编译好的 EXE,共 7 种形态。不要求按某个平台的 API 重写,现有项目补一份声明就能接进来。能力上限由你会的语言决定,不由平台决定。
开发简单。 接入只加一个 TOML 文件,原脚本一行不动。不用学 UI 框架,不用写调度代码,界面和定时都由盒子托管。
生态可复用。 不限定语言与接入框架,Python、Node.js、AutoHotkey、PowerShell 这些成熟社区的项目可以直接接;脚本之间还能互相调用,声明依赖后自动安装。
这三件事互为前提:源码可见,才敢复用别人的项目;语言不设限,复用才有价值;复用成本低,生态才会有人往里放东西。
它不适合谁
- 想改网页的,去用油猴,盒子帮不上;
- 用 macOS / Linux 的,目前不支持;
如果你手头有补一份声明就能接进来的脚本,欢迎提交到社区。
有踩过运行时部署的坑,也欢迎在评论区聊聊,这部分我还在持续改。
标签:Windows、Python、开源、效率工具、Node.js