MoreRSS

site iconrxliuli | 琉璃修改

Web 前端开发,在日本,喜欢自称「吾辈」让我很难受。。博客主要记录一些技术相关的东西
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

rxliuli | 琉璃的 RSS 预览

使用 GitHub Actions 全自动发布 Safari 扩展

2026-07-20 15:08:07

背景

我之前写过两篇关于 Safari 扩展的博客:转换 Chrome Extension 为 Safari 版本,以及 发布 Safari 扩展到 iOS 应用商店。它们解决的是”第一次怎么做成”。但随着我的 Safari 扩展越来越多,每次发版的手动流程变得越来越难以忍受:打开 Xcode、改版本号、Archive、Validate、Distribute,再打开 App Store Connect 选 bundle、填 release note、点提交——每个扩展十分钟起步,而且不能出错。

这篇文章介绍我现在的发布流程:改一下 package.json 里的 version,push 到 GitHub,然后就没有然后了。构建、签名、上传、提交审核,全部在 GitHub Actions 里自动完成。

1784531579982.jpg

最终效果

先看终态。这是一个真实项目里 Safari 发布相关的完整 workflow 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
safari:
needs: version
runs-on: macos-26
steps:
- uses: actions/checkout@v5
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v6
with:
node-version: 24
cache: 'pnpm'
- run: pnpm install
# 构建扩展并转换为 Xcode 项目,输出到 .output/My Extension
- run: pnpm build:safari

- uses: rxliuli/safari-webext-publish-action@v2
with:
# build 模式:构建、签名、上传 bundle 到 App Store Connect,但不提交审核
mode: build
project-path: '.output/My Extension'
bundle-identifier: 'com.example.my-extension'
team-id: 'ABCDE12345'
app-signing-identity: '3rd Party Mac Developer Application: Your Name (ABCDE12345)'
installer-signing-identity: '3rd Party Mac Developer Installer: Your Name (ABCDE12345)'
env:
APPLE_CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_MACOS_PROVISIONING_PROFILE_BASE64: ${{ secrets.APPLE_MACOS_PROVISIONING_PROFILE_BASE64 }}
APPLE_MACOS_EXTENSION_PROVISIONING_PROFILE_BASE64: ${{ secrets.APPLE_MACOS_EXTENSION_PROVISIONING_PROFILE_BASE64 }}
APPLE_IOS_PROVISIONING_PROFILE_BASE64: ${{ secrets.APPLE_IOS_PROVISIONING_PROFILE_BASE64 }}
APPLE_IOS_EXTENSION_PROVISIONING_PROFILE_BASE64: ${{ secrets.APPLE_IOS_EXTENSION_PROVISIONING_PROFILE_BASE64 }}
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}

safari-submit:
needs: [version, safari]
runs-on: ubuntu-latest
steps:
- uses: rxliuli/safari-webext-publish-action@v2
with:
# submit 模式:用刚上传的 bundle 创建新版本并提交审核,只需要 3 个 secrets
mode: submit
bundle-identifier: 'com.example.my-extension'
version: ${{ needs.version.outputs.version }}
env:
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}

配好这份 YAML 和它引用的 secrets,就是全部了。它用到了我为此写的两个工具:

为什么 Safari 需要专门的工具?Chrome/Firefox/Edge 的发布 WXT 已经做得很好了,一条命令的事。但 Safari 扩展本质上是一个 App——浏览器扩展的代码被嵌在一个 macOS/iOS 应用里,发布走的是 App Store,这意味着要面对 Xcode 项目、证书、签名、公证、provisioning profile 这一整套 Apple 的分发体系。复杂度的直观展示——这是一个全平台扩展需要的所有 secrets:

1784533468309.jpg

看起来吓人,但每一个都只需要配置一次。下面按顺序讲清楚每一步在做什么、每个 secret 从哪里来。

顺带一提:这套流程也是逐步演化出来的。最早我只用 wxt-module-safari-xcode 生成项目,仍然在 Xcode 里手动构建上传;后来 action 的 build 模式让我不再需要打开 Xcode;最后加上 submit 模式,才把最后”打开 App Store Connect 点两三分钟”的步骤也消灭掉。如果你想了解完整的手动流程(以及为什么它值得被自动化),见开头那两篇博客。

第一步:配置 WXT,把扩展转换为 Xcode 项目

Apple 官方提供了转换工具 xcrun safari-web-extension-converter,但它只做最基础的转换:生成的 Xcode 项目还需要手动设置版本号、App 分类、开发团队等。wxt-module-safari-xcode 把这些集成进了 WXT 的构建流程——运行 wxt build -b safari 时自动完成转换和全部配置注入。

1
2
3
4
5
6
7
8
9
10
11
12
13
// wxt.config.ts
import { defineConfig } from 'wxt'

export default defineConfig({
modules: ['wxt-module-safari-xcode'],
safariXcode: {
projectName: 'Your Project Name',
appCategory: 'public.app-category.productivity',
bundleIdentifier: 'com.example.your-extension',
developmentTeam: 'ABC1234567',
},
// ... other configurations
})

四个配置项的来源:

  • projectName:扩展的显示名,包含大写字母或空格都没问题。
  • appCategory:App Store 的应用分类,可选值见 Apple 官方分类列表
  • bundleIdentifier:全局唯一标识,惯例是反转域名 + 应用名,全小写、不含空格。
  • developmentTeam:你的 Apple Developer Team ID,在 developer.apple.com/account 的 Membership 页面可以找到。

1784536060862.jpg

运行 pnpm wxt build -b safari 后,Xcode 项目会出现在 .output/<projectName>,版本号已经从 package.json 自动同步(模块会同时更新 MARKETING_VERSIONCFBundleVersion,后者按 major×10000 + minor×100 + patch 换算成递增的数字版本——Apple 要求每次上传的 build number 必须递增,这个换算规则让它跟着语义化版本自动满足)。

真实项目的配置示例:https://github.com/rxliuli/imp-translate/blob/main/wxt.config.ts

第二步:准备 secrets

这是整个流程中最繁琐的部分,但每项只做一次。分三组。

证书(APPLE_CERTIFICATE_BASE64 / APPLE_CERTIFICATE_PASSWORD)

CI 里签名的原理是:把你的分发证书(含私钥)导出成 .p12 文件,以 base64 形式存进 GitHub secrets,action 在运行时把它导入 runner 上的临时 keychain 供 Xcode 签名使用。步骤:

  1. developer.apple.com 的 Certificates 页面 创建三张分发证书:Mac App Distribution(对应 “3rd Party Mac Developer Application” 签名身份)、Mac Installer Distribution(对应 “3rd Party Mac Developer Installer”。看到”Installer”别疑惑——Mac App Store 规定的上传格式就是签过名的 .pkg,它只是提交给 Apple 时的运输容器,用户实际安装走的是 App Store 自己的机制,永远不会接触这个 .pkg。action 内部用 productbuild 打包时需要这张证书签名),以及 Apple Distribution——注意一个命名陷阱:创建页面上它叫 “Apple Distribution”,但创建后在证书列表里只显示为 “Distribution“(Platform 列为 “All”),是同一张证书。它是跨平台的,负责 iOS 分发签名。创建过程中需要用本机钥匙串生成 CSR(证书签名请求),按页面提示操作即可。这三张证书都是账户级的,不绑定任何具体扩展——配置一次,所有项目通用。
  2. 下载证书后双击导入本机钥匙串,然后打开「钥匙串访问」→「我的证书」,同时选中这三张证书(确认每张都能展开看到私钥),右键导出为单个 .p12 文件——action 支持一个 .p12 内包含全部签名身份,不需要分开传。导出时设置的密码就是 APPLE_CERTIFICATE_PASSWORD
  3. 把 .p12 转成 base64 存入 secrets:base64 -i Certificates.p12 | pbcopy,粘贴为 APPLE_CERTIFICATE_BASE64

1784542838372.jpg

Provisioning Profiles(4 个 *_PROVISIONING_PROFILE_BASE64)

Profiles 页面 为每个 target 创建 App Store 类型的 profile。转换器生成的项目里有 App 和 Extension 两个 target,各自有独立的 bundle id——Extension 的 id 是主 id 加 .Extension 后缀(例如 com.example.my-extension.Extension),两个 bundle id 都需要先在 Identifiers 页面注册。macOS 和 iOS 各一套,所以一共 4 个 profile。每个下载后同样 base64 编码存入对应的 secret。

1784542904113.jpg

注意:这一步是按扩展算的——每新增一个 Safari 扩展,都要为它创建一套新的 profile。这也是所有 secrets 里唯一不能跨项目复用的部分(证书和 API key 都是账户级的,配一次全部扩展通用)。

App Store Connect API(APPLE_API_KEY / APPLE_API_KEY_ID / APPLE_API_ISSUER)

App Store Connect 的 Integrations 页面 创建团队 API 密钥,角色选 App Manager 就足够(最小权限原则,不要用 Admin)。创建后:

  • 下载 .p8 私钥文件——只能下载这一次,妥善保存。用 base64 -i AuthKey_XXXXXXXXXX.p8 | pbcopy 转成 base64 后存入 APPLE_API_KEY
  • 页面上的 Key ID 存入 APPLE_API_KEY_ID
  • 页面顶部的 Issuer ID 存入 APPLE_API_ISSUER

这组密钥承担两个职责:build 模式用它上传 bundle(替代了 Xcode 的 Distribute 步骤),submit 模式用它调用 App Store Connect API 创建版本并提交审核。

1784542967542.jpg

找不到这些页面入口是正常的,Apple 的后台并不以易用著称。一个现代解法:让能操作浏览器的 LLM 帮你导航到对应页面,比自己翻文档快得多。

第三步:理解 workflow 的两个 job

回头看开头那份 YAML,Safari 发布被拆成了两个 job,这个拆分有讲究:

safari(build 模式) 跑在 macOS runner 上——只有这一步真正需要 macOS 和 Xcode(注意必须是 macos-26 及以上,action 会检查 Xcode 26 SDK)。它做四件事:导入证书到临时 keychain、用 xcodebuild 构建并签名(macOS 产出 x86_64+arm64 通用二进制)、打包、通过 API 上传到 App Store Connect。跑完之后,bundle 已经出现在 App Store Connect 的构建版本列表里,等价于你在 Xcode 里点完了 Archive → Distribute。一个省心的细节:action 会自动从 Xcode 项目的 scheme 检测目标平台——项目里只有 macOS scheme 就自动跳过 iOS 步骤,反之亦然,不需要额外配置。

safari-submit(submit 模式) 跑在 ubuntu runner 上——它只调用 App Store Connect API,不需要 macOS,用便宜的 runner 就好。它等价于你在网页上手动做的那套:轮询等待 bundle 处理完成、创建新版本、关联刚上传的 bundle、填写 What’s New、提交审核。也因此它只需要 API 三件套这 3 个 secrets。开头抱怨过的”每次都要手动填 release note”也在这里被解决了:通过 release-notes 输入传入,不传则默认 “Bug fixes and improvements.”。

1784543097189.jpg

总结

诚实地说:如果你只有一两个 Safari 扩展,Xcode 的手动流程是可以忍受的,它替你隐藏了证书和签名的大部分复杂性,为它配置这一整套 secrets 未必划算。但如果你有三个以上的 Safari 扩展,或者单个扩展发版频繁,这条流水线会彻底改变你的发版体验——一次性的配置成本,换来之后每次发版为零的心智负担。我的十几个 Safari 扩展现在全部走这条流水线,配置完成之后,我再也没有为发版打开过 Xcode。

旅行 - 东福山岛

2026-05-29 20:31:47

前言

自去年年底去过一次之后,我就喜欢上了在海岛上旅行的感觉,相比于大陆近岸的海水,海岛周围的颜色透着一种天空的蓝。

cover

环岛徒步

这次旅行又走了一圈环岛徒步,只花了不到 3 个小时,比上次要快得多。

碧蓝之海,启动!
东福山岛-环岛-1

远方还能看到几艘渔船。
东福山岛-环岛-2

靠近岸边的海水反而泛着一些浅绿色。
东福山岛-环岛-3

顺时针走到大约 1/4 的地方,可以看到一个废弃的村庄,似乎目前只然有一些海防人员在此居住。
东福山岛-环岛-4

回头望去,几座岛屿在海平线的地方,却是无法上去的无人岛。
东福山岛-环岛-5

无论何时何地,总会不经意间看到大橘的身影。
东福山岛-环岛-6

一直都很喜欢这种海水不断拍打着礁石的画面。
东福山岛-环岛-7
东福山岛-环岛-8

正午,整个海平面都泛着白光。
东福山岛-环岛-9

远方矗立着一个不知名的三角形箭头,不知道是做什么的。
东福山岛-环岛-10

似乎还有人居住的村庄,但也主要集中在东北角接近轮渡码头的地方。
东福山岛-环岛-11

日出日落

当下午回来之后,就可以等待日落了,轮渡中心的码头就是一个不错的拍摄点,非常适合同时拍摄日落和岛上标志性的灯塔。
东福山岛-日落-1

半隐。
东福山岛-日落-2

稍微放大一些的画面。
东福山岛-日落-3

想要看日出非常依赖天气,我只有第二次去的时候看到了,第一次只看到了早起的渔船在海上忙碌着。
东福山岛-日出-1

这次比较幸运,看到了美丽的朝霞。
东福山岛-日出-2

随后,太阳从海平线的云层中慢慢升起。
东福山岛-日出-3
东福山岛-日出-4
东福山岛-日出-5

太阳完全升起,但天边的云彩仍然残留着一缕朝霞。
东福山岛-日出-6

两个过于勇敢的人爬上了那块大石头。
东福山岛-日出-7

最后仍然用刚上岛就能看到的灯塔作为结尾吧。
东福山岛-日出-8

结语

随着独立开发的时间增加,几乎完全失去了与其他人交流的机会,旅行成了排忧解闷的主要方式。尽管效果越来越不明显,但我仍然决定下半年出去,走完今年十个国家目标中剩余的七个。

旅行 - 新加坡 - 越南

2026-03-21 20:13:55

回来一个星期了,才开始动笔写新加坡和越南的旅行。相比于呆了 24 天的马来西亚,新加坡和越南都只呆了 3 天,尤其是因为越南航空晚点 16 个小时,导致越南之行实际上只有 2 天时间,非常匆忙。

新加坡

从新山走陆路口岸通过,如预料之中,新加坡的电子化极其发达,包括海关都是直接刷护照即可自动通关,完全没有人工环节。公交或地铁也都可以直接刷 Visa 卡,尽管国内的 Visa 双币卡在国外就是废物一条着实坑了我,但仍然难以掩盖信息化的发达。

新加坡 - 市区

住在市中心附近,所以下午先去逛了会街,但大部分小店的商品总感觉是从义乌直接批发的。
新加坡 - 市区 1

走了一会就能看到地标建筑金沙酒店,三栋楼上面放着一艘船的样子。
新加坡 - 市区 2

沿着河边走,还能看到远处已经停运的巨型摩天轮。
新加坡 - 市区 3

偶然看到一个有趣的设计,通过一个凹面镜反射出周围的景色。
新加坡 - 市区 4

经典 CBD 照片,建筑密集度相当惊人。
新加坡 - 市区 5

去附近的商场吃个饭,似乎在下层还有一些给小孩子玩的设施。
新加坡 - 市区 6

之后出来继续沿河边散步,一个奇怪的 Apple Store 映入眼帘。
新加坡 - 市区 7

之后,就是经典的新加坡城市夜景了,稍微拍了几张。
新加坡 - 市区 8
新加坡 - 市区 9

新加坡 - MLP 联名花展

完全出乎意料的是还看到了 MLP 的联名花展 Floral Fantasy,这是万万没想到的,当即去看了。

在路边走时突然就看到了这个,当机立断买票就去了。
新加坡 - MLP 联名花展 1

门口的宣传把 Rarity 和 Apple Jack 放在后面也是日常了,她们算是 Mane Six 中人气最少的两个 Pony。
新加坡 - MLP 联名花展 2

进入之后看到 Celestia 和 Luna 时旁边一个女孩直接说 “妈妈,看,是 Celestia”,真是温馨的小场景。
新加坡 - MLP 联名花展 3

不过 Celestia 制作的确实太棒了!
新加坡 - MLP 联名花展 4

还有 Twilight 和 Rainbow Dash,制作的也很棒。

新加坡 - MLP 联名花展 5
新加坡 - MLP 联名花展 6

唯一糟糕的就是没有 Starlight,不过她终究不是 Mane Six,也是可以理解的。

新加坡-海边

第二天去了附近的教堂,巨大的清真寺,旁边有一条网红打卡街道。
新加坡-海边 1

下午前往了海边,尽管新加坡是一个繁忙的货运港口,但海水的质量看起来也非常棒。
新加坡-海边 2
新加坡-海边 3

旁边的公园还能看到不知名的小鸟。
新加坡-海边 4

第三天仍然出去玩,超级喜欢新加坡的绿化,这种绿色看着太舒服了。
新加坡-海边 5
新加坡-海边 6

新加坡-圣淘沙岛

所以下午又跑去圣淘沙岛看海去了。
新加坡-圣淘沙岛 1

悬索吊桥晃晃悠悠,还挺好玩的。
新加坡-圣淘沙岛 2

碧蓝之海,启动!
新加坡-圣淘沙岛 3
新加坡-圣淘沙岛 4

一个完全不知何故水平生长的椰子树,几乎都长到了海里。
新加坡-圣淘沙岛 5

乘坐这个轻轨,离岛乘坐公交车回去准备前往越南。
新加坡-圣淘沙岛 6

越南

越南-胡志明市区

刚下飞机,就感受到了强烈的共产主义国家的气息,宣传几乎无处不在。
越南-胡志明市区 1

殖民时期遗留的教堂,新增的大概只有胡志明的雕像?
越南-胡志明市区 2

傍晚的广场上人来人往的还挺热闹。
越南-胡志明市区 3

旁边就能看到胡志明市的最高建筑了。
越南-胡志明市区 4

这个压迫感有点引起 PTSD 了。
越南-胡志明市区 5

这里过马路有时只有斑马线没有红绿灯只能硬过也是体验到了。
越南-胡志明市区 6

越南-博物馆与统一宫

第二天前往博物馆看到的东西还算有趣。
越南-博物馆与统一宫 1
越南-博物馆与统一宫 2

之后去了市博物馆(统一宫),建筑本身还可以拍几张,只是房间内部的装潢有些过于宣传了。
越南-博物馆与统一宫 3
越南-博物馆与统一宫 4
越南-博物馆与统一宫 5

楼上能看到下面巨大的广场和远方的街道。
越南-博物馆与统一宫 6

总结

无论是新加坡还是越南,我都没能留下太多美好的回忆,可能我还是呆的时间太短了吧。但世事总是不如人意,希望今年还能继续走完七个国家,考虑到世界上有接近两百个国家和地区,就算每年走十个,也需要二十年才能完成,但这显然是一个值得去做的目标,至少对我来说是这样。

旅行 - 马来西亚 - 下

2026-03-21 18:44:17

自从之前的亚庇之行结束后,我就出发前往了仙本那。那是一个很小的小镇,基础设施极其糟糕,而且总体上似乎相当贫困,路边经常看到乞讨者,小孩子围着人转希望给她们钱的例子屡见不鲜。海洋看起来相当不错,但烈日极其毒辣,我刚到的第三天就已经被晒脱皮了,不过似乎主要活动是潜水,如果不喜欢它,可能也不太值得待太长时间。之后前往了新山,临近新加坡的第二大都市圈,但对我而言过于熟悉了,所以呆的两天基本没出去,尤其是第二天只是呆在住的地方,几乎不值一提。

仙本那 - 市区

到住处暂存行李之后就开始在镇上闲逛,偶然看到河边的“水上人家”,大量简陋的铁皮屋矗立在水面上。完全缺乏管道系统(下水道)是最大的问题,而且小孩子还在旁边的水里游泳,但水质极其糟糕。

仙本那-市区 1

岸边的房子也不遑多让,尽显破败之相,难以理解一个旅游胜地经济发展为何会如此不堪。

仙本那-市区 2

而他们的交通基本上依赖于这种木板桥,我可以保证,这看起来很危险,实际上一点也不安全。

仙本那-市区 3
仙本那-市区 4

但这儿的小孩子有一种国内好久没有看到的乐观和天真了,基本上国内的小孩子一看到人就跑了,根本不会让人拍照。

仙本那-市区 5
仙本那-市区 6

偶尔在楼房之间的通道中还能看到在街头生活的老人。

仙本那-市区 7

仙本那 - 出海

前面说了那么多糟糕的现状,但这边的海水确实非常棒,我的意思是,

仙本那-出海 1
仙本那-出海 2
仙本那-出海 3

不幸的是,贫困似乎是一种顽疾,即使在这种旅游区域,仍然有大量巴瑶族人在卖椰子,或者让小女孩爬上梯子向游客乞讨。

仙本那-出海 4
仙本那-出海 5
仙本那-出海 6

仙本那 - 马布岛

在报一日游浮潜去了一次马布岛之后,就一直想去岛上的居民区域转转,所以之后确实去住了一天水屋。

本地居民区可以看到小孩子聚居在一起玩,在手机时代之后国内很少看到这种景象了,往往是每个小孩子都抱着自己或父母的手机玩,这在我看来是一种悲哀。

仙本那-马布岛 1
仙本那-马布岛 2
仙本那-马布岛 3

岛上的海滩更是棒极了,

仙本那-马布岛 4
仙本那-马布岛 5
仙本那-马布岛 6

不幸的是,贫困总是如影随形,海边的房屋仍然缺乏管道系统,所有的脏水都是直接排入海洋中,而这对于健康而言绝对没有任何好处。

仙本那-马布岛 7

疯狂的是,如此年幼的孩子,只是坐着一块泡沫板,或者一个气阀,就出海帮助维持家庭生计了。

仙本那-马布岛 8
仙本那-马布岛 9
仙本那-马布岛 10

日落总是短暂的,只有远方废弃的钻井平台亮着灯。

仙本那-马布岛 11
仙本那-马布岛 12

总结

总的来说,如果你喜欢潜水,仙本那几乎每天都有出海潜水的团,但如果像我一样更喜欢拍照,可能仙本那呆个 3~4 天也足够了。接下来,我将前往新加坡,看看传说中坡县是怎么样的。

旅行 - 马来西亚 - 上

2026-03-09 20:22:56

前言

在春节之后,我来到了马来西亚旅行。这是我出国旅行的第三个国家。在此之前,我曾经在日本呆了半年,在关西走了一些地方,回国后又花了几个月的时间走完了整个中国的所有省份和直辖市,所以某种程度上我的阈值确实提高了。因此,直到目前为止,马来西亚给我的体验,可以用一个字来形容:烂。

吉隆坡

落地首先到了吉隆坡,基建水平还行,但室内导航很困难,而且售票机只收取小额纸币导致我一度没有零钱可用,直到后面办了一张 TNG 的卡,但这张卡在槟城时公交完全用不了,后面的其他地方几乎没有公共交通可用,所以是的,基本上是个坑。去看了那个大佛像和彩色楼梯,确实不错,至少值得一去。

吉隆坡标志性的大佛像。
吉隆坡 1

彩色楼梯栏杆上的猴子。
吉隆坡 2

登至台阶顶处往上看。
吉隆坡 3

独立纪念碑,附近值得多拍几张,有巨大的清真寺。
吉隆坡 4
吉隆坡 5
吉隆坡 6

顺便骂一句 Booking,收费方式极其混乱,我经常忘记一个预定到底有没有付钱,有些是到店付款,有些是线上支付,而且使用者没办法决定,真是糟透了。

金马伦高原

抱团一日游简直太糟糕了,除了最早的森林徒步,后面凑数的都是什么臭鱼烂虾。我自己也走了两条热门的原始森林徒步路线,确实很原始,大部分都是石头、泥土和树根组成,但也仅此而已。

徒步起点,直接楼梯加绳子真的很劝退,我差点就放弃了。
金马伦高原 1

山上云雾缭绕,山下的建筑却早已变成了水泥盒子。
金马伦高原 2

第二天抱团走的森林徒步,看到了下面色彩斑驳的森林。
金马伦高原 3

朝露与青苔,可能是森林徒步中的一种“小确幸”了。
金马伦高原 4

金马伦高原有名的茶园。
金马伦高原 5

还算幸运的拍到了纹胸花蜜鸟。
金马伦高原 6

一日游最后一个佛寺景点实在让我蚌埠住了。
金马伦高原 7

作为推荐的汽车订票软件 easybook,web/app 设计水平之烂,ux 体验之痛苦,我会说在国内那些毒瘤 app 都是少见的。尤其是支付部分,每次购买时都必须填写相同的信息,而没有自动保存选项。微信支付集成就更有趣了,在 app 上显示一个二维码让扫码支付,这是什么鬼才设计?即使如此,也无法正常支付,我怀疑是否有人测试过。

槟城

据说是华人聚居的城市,面朝海边吃着当地小吃确实还不错。但我本身对壁画和艺术并不太感兴趣,然后升旗山真的真的没什么,我的意思是和香港的太平山没什么区别。树冠步道也没有什么,远不及后面兰卡威的天空之桥。

天还没亮时拍摄的槟城。
槟城 1

日出。
槟城 2

树冠步道。
槟城 3

一只蝴蝶。
槟城 4

醒来的槟城。
槟城 5

市政府。
槟城 6

槟城海港。
槟城 7

电子化是依托答辩,除了 711 之类的连锁大部分还是要现金,有的地方看起来有扫码选项实际不能用,公交卡不能用来刷(滨城)公交是什么鬼?

兰卡威

截至目前为止唯一感觉还不错的城市,在海边拍日落还不错,天空之桥值得一看,旁边的七仙井瀑布也可以顺便取一下,那里可以泡水,我还看到了水巨蜥,不过如果要租摩托车需要摩托车证,这是一开始没有预料到的。

刚到就看到了日落,太幸运了。
兰卡威 1

天空之桥,真的非常棒。
兰卡威 2

大海的分层实在太漂亮了。
兰卡威 3
兰卡威 4

站在桥上往下看去。
兰卡威 5

山泉泡水,夏天这样很舒服。
兰卡威 6

只是需要小心“可爱”的邻居,这显然是它的地盘。
兰卡威 7

第二天早上骑着自行车就去看日出去了,这很棒,没有驾照也可以租个自行车。
兰卡威 8

还能去机场附近近距离拍大飞机降落。
兰卡威 9

亚庇

亚庇的京那巴鲁山登顶需要专门的设备和门票,而下面的公园其实没有太多值得说的,东南亚的原始丛林远不及预期,原本我会以为类似于西北野生动物很常见的情况,也许确实如此,但可能都太隐蔽了。一些景点也真的太糊弄人了,例如亚庇的红树林据说可以看到长鼻猴和萤火虫,怎么说呢,确实如此。能看到,但也仅此而已了,萤火虫和预期完全不同,完全不是河面上大片的萤火虫,只是一棵树上聚集了一些萤火虫,也没什么大不了的。值得花 8 个小时和 RM190 吗?至少我觉得并不值得。

尚未降落时的云海。
亚庇 1

山上到处都是雾,几乎什么都看不清。
亚庇 2

复杂的树根结构。
亚庇 3

第二天早晨雾气稍微散去之后。
亚庇 4

休息一天后前往红树林,河旁边就有一条鳄鱼。
亚庇 5

确实看到了长鼻猴,但只能说还好?
亚庇 6

日落还算不错,萤火虫就别提了,只是一点点。
亚庇 7

必须吐槽的是公共交通的水平之低,我的天呐,没想到现在还要在路边挥手随机拦车,真是见了鬼。这比西北那边的公共交通水平还低,很难有什么准确的预期。

结语

来之前对马来西亚的大海和丛林比较感兴趣,可以说,后者令人大失所望。至于大海,传闻国外最糟糕的海岸也比国内的更好,就吾辈实际经历而言,滨城、兰卡威、亚庇 三者中只有兰卡威有点意思,其余都是什么鬼。
目前正在前往仙本那,也许仙本那的大海会改变我的想法,但我想先写下这种感觉。

Browser Extension Dev - 08. 发布 Chrome Web Store

2026-01-26 08:27:04

前言

在之前的 7 篇博客中,我们依次了解了一些扩展开发中的基本概念,并且每一篇都附上了一个扩展示例。现在,我们终于要演示如何发布扩展了。下面我们将演示如何将之前做的自动冻结不活跃标签页的那个扩展发布到 Chrome Web Store 中,还记得吗?就是我们在 Browser Extension Dev - 04. Background ScriptBrowser Extension Dev - 05. 存储和配置 中作为示例的那个扩展。

步骤

准备 Chrome Web Store 发布账户

  1. 首先按照官方文档注册开发者账户,需要一个 Google 账户并且支付一次性注册费用 $5 即可完成。参考:https://developer.chrome.com/docs/webstore/register
  2. 然后继续完成设置,主要是设置开发者账户名称,以及验证邮箱,没什么太复杂的。参考 https://developer.chrome.com/docs/webstore/set-up-account

注册完成后打开 https://chrome.google.com/webstore/devconsole/ 应该可以看到如下页面。

1769515955075.jpg

构建扩展包

接下来,开始演示如何从构建到最终发布扩展。

首先,在项目中打开终端,然后运行 pnpm zip,应该会看到类似下面这样的输出,可以看到 Chrome 扩展已经被正常打包成 zip。

1
2
3
4
ℹ Zipping extension...
✔ Zipped extension in 12 ms
└─ .output/05-storage-and-configuration-0.0.0-chrome.zip 13.37 kB
Σ Total size: 13.37 kB

在 .output 目录下找到这个文件,记住这个路径。

1769517095997.jpg

上传到 Chrome Web Store

然后打开 https://chrome.google.com/webstore/devconsole/ 并点击右上角的 New Item 按钮。

1769517209416.jpg

选择刚刚找到的 zip 文件上传,此时遇到了一个错误,提示 The manifest has an invalid version: 0.0.0. Please format the version as defined,也就是版本号不能为 0.0.0

1769517322845.jpg

使用 pnpm version patch 将版本号增加到 0.0.1,然后重新运行 pnpm zip 构建并上传,即可看到扩展发布管理页面。

1769517483216.jpg

配置商店展示信息 (Store Listing)

其中,对于发布而言,最重要的两个标签页是 Store listing 和 Privacy。前者用于配置扩展在 Chrome Web Store 中的展示信息,例如简介、分类、图标和截图等等,后者则是权限使用说明和隐私政策链接。
对于这个扩展而言,选择分类为 Productivity > Tools,语言选择 English。

1769649613093.jpg

然后从 public/icon 目录选择 128.png 图标作为在商店显示的扩展图标。要截取精确 1280x800 像素的截图可能有点麻烦,但可以直接使用 https://squoosh.app 来调整截图的大小,使用 Resize 功能调整截图尺寸到 1280x800 就好了。

1769518272363.jpg

PS: 如果你使用 mac,可以使用小工具 Window Resizer 来将窗口尺寸修改为指定大小。
参考 Chrome 官方发布文档 https://developer.chrome.com/docs/webstore/publish

配置隐私政策 (Privacy)

切换到 Privacy 标签,可以看到有几个主要区域

  • Single purpose:单一用途说明,简单来说就是用一两句话说清楚扩展是做什么的
  • Permission justification:权限使用说明,注意最后的 Are you using remote code?,Chrome 禁止使用远程代码,某些库(如 zod)可能会不小心引入远程代码,需特别留意
  • Data usage:数据收集说明,选择扩展收集了什么数据,如果扩展是本地运行的,那么不需要选择任何选项
  • Privacy policy:隐私政策链接,这是唯一需要外部托管的内容,可以在个人域名上托管它,如果是开源的,也可以直接放上 GitHub 相关文件的链接。这是一个示例:https://rxliuli.com/webstore/privacy/

1769649228557.jpg
1769649249668.jpg

提交审核

按下 Save draft 按钮之后,如果 Submit for review 按钮可用,那就说明可以提交扩展进行审核了。

1769649204651.jpg

提交审核后,将会进入审核队列,通常需要几天甚至更长时间进行初次审核,所以还请耐心等待,某些使用高风险权限(例如向所有网站注入脚本)或者针对高风险网站(当然,吾辈是在说 YouTube)的扩展可能需要等待更长时间。

1769649344613.jpg

总结

至此,浏览器扩展开发的基础内容就介绍完了。后续可能会有一些进阶主题的番外篇,比如国际化、GitHub Actions 自动发布等。

如果还对发布 Safari 扩展并上架 App Store 感兴趣,可以查看我之前写的博客 转换 Chrome Extension 为 Safari 版本发布 Safari 扩展到 iOS 应用商店。提醒一下,这非常复杂,且开发者账户无试用期,必须满足 1)有一台 macOS 电脑并且安装 Xcode 等开发工具 2)开通 App Store 开发者账户并支付 $99/年的费用。