一个不能上架应用商店的扩展,怎么发给几十号非技术同事?
约束很硬:
- 没有管理员权限(所以任何需要提权的方案直接出局);
- 很多人不用命令行(所以要有双击就能跑的入口);
- 要能更新,而且更新不能比安装更麻烦。
最后的形态是一条 PowerShell:
irm https://example.com/install.ps1 | iex外加一个给不用命令行的人双击的 .cmd,它只做转发——去服务器拉同一份 .ps1 来跑,所以本地那份 .cmd 永远不会过期。
脚本做什么
读安装目录里的 manifest.json → 判断首次还是更新 ↓[1/4] 下载最新 zip[2/4] 解压 + 校验(找 manifest.json 确认是合法扩展包)[3/4] 部署到固定目录(先清空再拷贝,避免残留旧文件)[4/4] 清理临时文件 ↓打印本次更新内容 ↓首次 → 开扩展管理页 → 弹出资源管理器并选中扩展文件夹 → 提示拖进去更新 → 只开扩展管理页 → 提示点「重新加载」 ↓匿名上报一条安装记录(fire-and-forget)[2/4] 里那句「找 manifest.json 确认」不是多余的:zip 里可能多套一层目录(取决于打包时的当前目录),所以要递归找到含 manifest.json 的那一层,把它当真正的根。
$maniFile = Get-ChildItem -Path $TmpExtract -Filter 'manifest.json' -Recurse | Select-Object -First 1if (-not $maniFile) { Write-Host ' [错误] 压缩包内未找到 manifest.json,不是合法的扩展包。' -ForegroundColor Red return}$srcRoot = $maniFile.DirectoryName$newVer = (Get-Content $maniFile.FullName -Raw -Encoding UTF8 | ConvertFrom-Json).version顺带也解决了「装了但浏览器说不是扩展」这类模糊失败——在部署之前就明确报错。
★ 老用户绝不搬家
这是整个脚本里最贵的一条。
某个版本我把安装目录往下挪了一层(从 %USERPROFILE%\ext 挪到 %USERPROFILE%\vendor\ext),为的是资源管理器打开父目录时能看见并拖走扩展文件夹本身。
如果更新脚本顺手把老用户也迁过去,他们的扩展会当场失效——浏览器认的是加载时那个绝对路径,目录一搬,扩展就断了,而且用户完全不知道发生了什么。
所以:
# 旧布局里有 manifest.json → 就继续用旧目录,什么都不动$legacyVer = Get-LocalVersion $LegacyDirif ($legacyVer) { $InstallDir = $LegacyDir Write-Host (' 检测到旧版安装位置,将原地更新:' + $InstallDir) -ForegroundColor DarkGray}用户之间路径不一致是预期行为,不是 bug。 卸载脚本两个位置都清,父目录只在空的时候才删。
任何「顺手把历史数据规整一下」的想法,在分发场景里都要先问一句:规整失败的那部分用户,会以什么方式发现? 如果答案是「功能突然没了、而且没有提示」,那就别规整。
chrome:// 打不开,只能抢焦点粘地址
这是 Windows 上做浏览器扩展分发绕不过去的一个坎:
chrome://extensions 这类地址无法用命令行参数打开。 浏览器会忽略它(安全设计)。
唯一可行的办法是模拟用户操作:
Set-Clipboard $UrlActivate-Window $proc # 抢前台焦点$wshell.SendKeys('^t') # Ctrl+T 新标签(浏览器已在跑时才需要)$wshell.SendKeys('^l') # Ctrl+L 聚焦地址栏$wshell.SendKeys('^a') # 全选,清掉占位内容$wshell.SendKeys('^v') # 粘贴$wshell.SendKeys('{ENTER}')「抢前台焦点」还要绕过 Windows 的前台锁定保护——不绕的话 SetForegroundWindow 会静默失败,你的按键会打到 PowerShell 窗口上(于是用户看到一串 ^l^a^v 被输进了控制台)。
这套东西脆,但没有替代方案。所以每一步都要能失败得优雅:抢焦点失败 → 提示用户手动打开;粘贴失败 → 地址还在剪贴板里,告诉他自己粘。
三个「顺序不能换」
① 先开扩展页,再弹文件夹
explorer.exe 会抢前台焦点。如果先弹文件夹,后面那一串 Ctrl+L / Ctrl+V 全会打到资源管理器窗口上,导航直接失败。
Open-ExtPage ← 先Open-InstallFolder ← 后(explorer 抢焦点,但此时已经不需要按键了)② 剪贴板:先放 URL,再放安装目录
Send-UrlToAddressBar 要靠剪贴板粘贴。所以安装目录只能在开完扩展页之后再写剪贴板,否则会把 URL 顶掉——用户按 Ctrl+V 粘出来的是一个网址而不是目录。
而且只在扩展页打开成功时才覆盖:失败的话剪贴板里那个 URL 还要留给降级提示用。
③ 选浏览器的 Read-Host 会阻塞
装了两个浏览器时,脚本会问用户装到哪个。这一步是阻塞的:
Open-ChosenBrowserExtPage ← 在这里等用户输编号 ↓ (输完回车) 开扩展页 ↓Open-InstallFolder ← 到这一步资源管理器才弹出于是有人报「资源管理器没自动弹出」。那不是 bug——他卡在选浏览器那一步还没输编号。
排查这类问题的判定顺序:① 是不是卡在 Read-Host;② 目录里已经有 manifest.json(= 装过了)→ 走的是更新分支,按设计就不开文件夹;③ 才轮到怀疑 explorer /select 本身。
安装统计:fire-and-forget
部署完成后匿名上报一条记录:版本、升级前版本、是否首装、渠道、系统 build。
try { Invoke-RestMethod -Uri $PingUrl -Method Post -Body $json -ContentType 'application/json' -TimeoutSec 5} catch { } # ★ 全吞。统计失败绝不能拖累或中断安装三条边界:
- 5 秒超时 + 异常全吞。 内网机器连不到公网时静默跳过,用户完全无感。
- 不含姓名、工号、主机名、用户名、IP → 定位不到具体个人。
- 来源 IP 刻意不由脚本上报:请求到达服务端时公网 IP 本来就在代理头里,让客户端再去问第三方查 IP 纯属绕远路加一个失败点。
还有一条认知上的:这是「安装事件流」,不是「在用版本分布」。 装完再也不升级的人,永远停在他那条旧记录上。后者得由扩展自身上报。
打印「这次改了什么」
安装脚本跑完打印本次更新内容——这是用户唯一会读的地方。
function Show-Changelog { # 读包内的 changelog.json,按新版本号取对应条目 # 读不到 / 没有对应版本 → 静默跳过(兼容老包)}配套的是打包时的一道硬闸:更新日志里没有当前版本的条目,直接打包失败。
因为漏写更新日志从来不会报错——不写,打包照过、安装照跑,只是用户装完看不到「这次改了什么」,而那恰恰是他最该看见的一句话。
凡是「漏了会静默出错」的事,都做成闸门。
三条分发渠道,一份业务逻辑
| 渠道 | 入口 | 谁用 |
|---|---|---|
| 公网 | irm https://example.com/install.ps1 | iex | 会用命令行的 |
| 双击 | 下载 i.cmd 双击 | 不用命令行的 |
| 内网 | 同一份脚本的内网副本 | 连不到公网的 |
关键是只有一份 .ps1。 .cmd 不重写任何逻辑,它只做一件事:转手调 PowerShell 跑那份 .ps1。
用 batch 重写那些逻辑(读 JSON、操作剪贴板、抢焦点)是做不到的——batch 里没有对应的能力,硬翻必然行为漂移,然后你有两个入口要同步维护。
i.cmd 还会云端失败自动降级内网,所以一份文件走遍内外网。
最后:别双击那个 .ps1
这条要写进脚本的注释里,因为它反直觉。
.ps1 是无 BOM 的 UTF-8(必须如此,带 BOM 会让 irm | iex 报错)。而 Windows PowerShell 5.1 从磁盘读无 BOM 文件时按系统 ANSI 代码页解码——中文 Windows 上是 GBK。
于是中文注释会把下一行真代码吃掉,实测这个脚本丢 58 行。
好消息是丢行后语法必然崩坏,PowerShell 先整体解析再执行,所以结果是「大声报错、一行不执行」——不会装坏,但也装不上。
要本地跑就用 .cmd(它显式按 UTF-8 解码)。完整的编码地狱在另一篇。
可以直接抄走的模板:/showcase/ps-ext-installer —— 安装 / 卸载 / .cmd 引导器三个文件,域名全部用占位符。
回头看,这套东西真正难的从来不是「把文件放到位」——那是十行 PowerShell。
难的是让一个不知道什么是扩展的人,在自己的机器上,一次成功。剩下那五百行全在处理这件事。