Author Archives: Jie Wang

懒猫微服的非典型性玩法

最近入手了一个小众新产品:懒猫微服。先在此感谢懒猫微服 CEO 张勇(Andy Stewart)因为我是 yetone 手搓的 OpenAI Translator 的忠实用户来提供的优惠券。 😂

懒猫微服

很多人在第一次看到懒猫微服介绍的反应几乎都是“这不就是个 NAS 吗?”其实我也不例外。我本身就拥有一台群晖 NAS,所以经过一段时间的把玩之后,我意识到懒猫微服看似 NAS,其实在数据和服务稳定性方面远不如 NAS,但是他的性能和可玩性方面远超 NAS。懒猫微服和 NAS 完全是两个赛道上的东西。

懒猫微服 NAS

为什么我会说懒猫微服在数据和服务稳定性方面远不如 NAS,原因有二:

  1. NAS 原生支持磁盘冗余阵列(RAID);懒猫有两块数据盘,但并没有启用 RAID。懒猫微服支持通过外接硬盘来实现 Btrfs 实时备份,虽然对于无 NAS 用户这的确是一个不错的折中方案,但是作为一个已经有 NAS 的玩家,我肯定不会考虑将重要数据迁移到懒猫微服中
  2. NAS 通常都是双网口。虽然 NAS 的双网口有很多有趣的玩法,但是我一般都使用绑定模式来保证即便一个网口脱落了,通讯不会被影响。不过 NAS 基本上都不原生支持 WiFi,懒猫却支持。网线直连 + WiFi 倒也算是懒猫的一个折中方案

所以目前我不会考虑用懒猫微服替代掉我的 NAS,当然这本来也不是懒猫微服的市场定位。

懒猫微服 > NAS 在不考虑上一个段落问题的前提下

同时懒猫微服也有不少吸引我的地方:

  1. 它的性能远高于一些常规 NAS,但是同性能的高端 NAS 售价却远高于懒猫微服。目前受限于 NAS 的性能,我很多想部署的东西都被暂时搁置了,而现在我可以把它们都部署在懒猫微服了,同时我也可以把大部分应用从 NAS 迁移到懒猫微服了。NAS 的定位将会是一个纯数据备份工具和影音中心。
  2. 懒猫微服应用市场内容丰富,且有自动网络穿透。其 SDK 支持跨平台和多端,社区也很活跃。所以可玩性非常强。
  3. 懒猫微服官方应用的一些能力和群晖对标应用所没有的。比方说,受益于懒猫微服比 NAS 更高性能的芯片,懒猫官方相册通过大语言模型的加持支持通过自然语言来检索照片。同时懒猫官方相册的新版本将会支持相似照片发现和清理。群晖的逆优化也是远近闻名的,群晖曾经的 Moments 应用是支持相似照片发现和清理的,但是后来新一代相册应用 Synology Photos 上线后却硬生生将这个非常有用的功能给去掉了。而且用户社区一直向群晖喊话恢复这个功能,但是官方毫无回应。再者,群晖官方也迟迟不开放 Synology Photos 的 API,社区想自己做插件来搞定都不行。在有懒猫之前,我不得不使用第三方工具定期扫描 NAS 的照片目录也找出相似图片并一一清理,非常的麻烦。

非典型性玩法

为了能够既不将重要数据如照片迁移到懒猫微服,又能使用到懒猫微服的官方应用的有意思特性,我利用 Syncthing 整出了一个非典型玩法。

Synology Photos & 懒猫相册双持

首先,我对懒猫相册和 Syncthing 的能力做了个评估,确认我的非典型性玩法是可行的

  1. 懒猫相册不仅仅可以收录应用端上传的图片和视频,也可以支持直接复制到“图片”目录的内容。
  2. Syncthing 目录同步配置支持指定目录的过滤。

然后,我开始按照下图开始双持部署。 😂

https://www.plantuml.com/plantuml/png/TPB1IiCm7CRlUOg_FUbXUm0POxw023kAXz1aQw5h9CqEGpo9Jd03aKsGUPmWY4MyJ9HrBxEaUmoZ6SqQvwaqt_V_VVt-JICHc8jk9q8x8g0T2XqiA0znXNL55COKOLym3ldeMLtF46fnh3sUwj_bWnUPfUfnwC4P0ZZbpAT90aR99n_oQh1nx3LxiI-2C6w3fuzRB2AKWsTy-IILyx4Mpek0F9kgXtiuF6XglCDX4Mltu2XWWYNUrep_8duJVCm9o4csp6vAQFavqYtjf1dHx-QJLcacf_qVWDa5r2h7GKW8ZK_gQnmo2AXLhRAcc4T9MGEjPBLIVuIANZgpxZI2VNaxKAzFoDcbiTh9mhWhP--hHLzzJKs6lBnGupSrJCtadfks1L5ie4ggzJ-_HDkMyuMsEBRDcs4zvJRQszs6TDePdyj43HeJVPE_0G00
Synology Photos & 懒猫相册双持
  1. 我在群晖 NAS 和懒猫微服都安装了 Syncthing,并设置将 NAS 中的相册目录同步到懒猫微服相册目录的一个子目录。同时在懒猫微服的 Syncthing 端设置同步时忽略掉群晖 NAS 的元数据目录 @eaDir
  2. 仅开启 Synology Photos 手机端相册备份功能,并保持懒猫相册手机端的相册备份功能的禁用状态,这样子可以防止同一张照片被重复备份。

这样子,既能依托于懒猫微服的高性能使用懒猫相册来管理,也能使用 NAS 来保管好我的各种回忆。无论我在 NAS 侧还是在懒猫微服侧对相册内容做操作,同时被同步到另外一侧。

未完待续

我现在还一直在挖掘懒猫微服的新玩法,比方说最近我刚在懒猫商城上架了一个集成了 OllamaAnythingLLM,部署了一个轻量级的 RAG 实现。

当然懒猫微服也存在非常多的问题,比方说,非常晦涩难读和不完整的 SDK 文档,充斥着大小 Bug 的系统、SDK 和官网,草台班子般的客服支持。我在微服上面移植和开发应用的整个过程,如同在垦荒。但是瑕不掩瑜,懒猫各方面高效的迭代优化和补丁,极其快速的客服响应等方面,都给我一种蒸蒸日上的感觉。

作为一个公司的此类产品的第一款,让我心内也有了足够的预期。所以,正是因为这种不完善,反而让我觉得这种垦荒还蛮有意思的,让我愿意继续挖掘懒猫微服的可玩性。

ChromeOS 带来的惊喜

我一直想有一个称手的可以随身携带的设备,可以用来写作、看看视频或者碎片时间写写代码。尝试过用 Macbook 来充当这个角色,但是觉得背在包里面实在是太重了。

我考虑过很多方案,但是都又一个一个被我自己毙掉了,比方说:

  • GDP Win:实在是对 Windows 提不起兴趣,另外键盘有点过于小了。如果真的用这个写博客和代码,指尖得长老茧。而且价格过于感人,看到想哭 🥹
  • DevTerm:Linux 系统,赞!可惜屏幕过于小了,不适合长时间使用。而且续航也是个问题
  • PinePhone:Linux 手机搭配原装键盘套件,就和 DevTerm 一样了。如果发现不好用,还能直接刷成 Android 系统,当 Android 手机用。官方都声明了,这个产品目前还是发展阶段,使用过程中会遇到一些不可预知的问题,建议爱折腾的用户购买。为了防止买了个祖宗回家,从我玩设备变成设备玩我,果断放弃。

正在茫然地时候,突然脑海中蹦出来一个词:ChromeOS 。我原来对 ChromeOS 了解还停留于:它所有的应用都是基于 Web 运行于 Chrome 之上的,简而言之,他只能运行 Web 应用和浏览网页;同时他的轻量级使得 Chromebook 很便宜且性价比很高。经过一番调研之后,我惊讶地发现 ChromeOS 已经进化了很多,他的功能性和易用性早已今非昔比,最终我入了一台。当然我并没有打算在本文介绍 ChromeOS 和 Chromebook 的功能和特性,毕竟这些东西官方网站都有。我更想分享的是,在整个初体验过程中,所看到的意料之外的惊喜。

三系统深度集成

现在的 ChromeOS 不光仅仅支持网页、Web 应用和渐进性网络应用(PWA:Progressive Web Apps),同时也兼容 Android 应用和 Linux 应用。它并不是简简单单地在 ChromeOS 里面运行了 Android 虚拟机和 Debian 虚拟机。整体上体验非常类似于 Parallels Desktop 的融合模式。Chrome 浏览器、Web 应用、PWA、Android 应用和 Linux 应用,可以如同原生应用一样无缝平铺在桌面上。

如上图,我们能看到左上角的 Google Play 和右下角的 Kodi,这两个都是 Android 应用。我们也能看到左下角 Linux 版本的 LibfreOffice,右上角和右侧中间的分别是 ChromeOS 内置 Chrome 浏览器和 Outlook 的 PWA。虽然三种系统工具集的区别引起了少许外观上的差异以及沙盒隔离造成权限上的差异,但是整体使用体感上没有明显地差异。

Android 应用串流

如果和 Chromebook 配对的搭载 Android 13 及以上版本的手机,手机中运行的应用可以直接串流到 ChromeOS 中。串流的形式,并不是投屏,而是将手机中的应用如上图融合模式中的原生应用一般运行在 Chromebook 上。其实这个应用并没有被安装在 Chromebook 上。

全局扩展支持

我一直在努力提升我的英语水平,当然也包括阅读效率。最近发现了一个 Bionic Reading 的阅读方法,能够在一定程度上帮助我提升阅读效率。

于是乎,我开始使用 Jiffy Reader 扩展来自动把我选定的网页转换成 Bionic Reading 模式。我一直试图寻找一个工具能够全局的将尽可能多的展示内容都转换成 Bionic Reading 模式,但这显然几乎不可能。

ChromeOS 上面应用主流,无非就是网页、Web 应用和 PWA,Android 和 Linux 应用毕竟是小头。而且前者都是运行在 Chrome 之上的,简而言之,Chrome 扩展在这些应用中都会其效果,包括 Jiffy Reader。我对 Outlook 是开启 Bionic Reading 模式的,所以在上面那个多窗口融合截图里面,我们可以清楚看到 Outlook 菜单标签中 Home、View 和 Help 的一些字母是加粗的。

再简而言之,各位各种有意思的私房插件不光只能在浏览器里面使用了 😂

模式无缝切换

ChromeOS 的布局是支持笔记本和平板两种模式的。它的平板模式叫 Touch UI Layout,体验就类似于安卓平板或者安卓手机,而笔记本模式当然就类似于笔记本了。同时可设置为通过识别是否连接鼠标或者触摸板来实现自动无缝切换。

如上图,我入的是一个分体式 Chromebook。每次接上和卸下兼作保护盖的键盘,Chromebook 都能顺畅地在两个模式之间无缝切换,非常的方便。

先写到这了,有什么新的惊喜再慢慢补充吧。

一个可以给网页中中文文字标注拼音的脚本

本文启发于小众软件简繁自由切换 – 为网页添加拼音、简繁转换[油猴脚本],支持自动将 yyds 转换回正经中文一文,主要是为了向正在学习中文的外国朋友介绍一下这个油猴脚本。所以熟悉汉语的朋友可以直接访问如下小众软件站点进行阅读。

当然如果对本文的英语版有兴趣的话,请移步此页面

如何部署多合一端对端加密即时聊天服务

在中国的时候,即时聊天工具(后面以 IM – Instant Messaging 代替)方面,基本上是微信走天下。安装一个微信几乎就能和所有朋友保持联系了。

到了新加坡,IM 真的是百花齐放,再加之不同人出于不同的原因,对于 IM 也有不同的偏爱,导致你需要安装多个 IM 才能和所有新朋友保持联系。简单数了一下,我现在手机上,除了微信,还安装了 WhatsApp、Telegram、Signal、Line、Google Chat、Linkedin 等等。虽然我个人更偏爱 Telegram,但我还不得频繁地在不同的 IM 之间切换,着实让人头疼。

市场上也不乏出现过一些多合一的 IM但其实都不怎么理想无非就是下面两种

  1. 通过 Frame 将这些 IM 的网页版集合起来。这种在桌面上体验还勉勉强强;在手机上,面对这些其实是更适配大屏的 UI 简直是噩梦。
  2. 通过抓包或者分析网页版将内容提取出来后,重新组装一个集成度更高更美观的 UI 出来。坦白说,这种方式让我很担心数据安全问题,毕竟它会破坏掉这些 IM 原本端对端加密的可靠性。

然而,前段时间我发现一个叫 Beeper 的服务,让我看到了一点曙光。

根据官网的介绍,这简直是满足我对一个多合一 IM 的所有想象。于是果断注册,然后被现实啪啪打脸!

已经排了好几个月了,前面依然还有十几万人,简直是遥遥无期。然后我脑海中突然蹦出来一个念头,既然他是基于开源协议 Matrix 建设的,我完全可以尝试自己部署一下。

简单调研了下,Matrix 彻底让我心动了:

  1. 支持桥接,可以与 WhatsApp、Telegram、Signal 等等其他 IM 进行桥接,通过 Matrix 可以收发所桥接 IM 服务的消息。
  2. 支持端对端加密。它的加密算法是开源的,并已经被各种应用场景如 Signal 证明可靠的。由于它本身对端对端加密的支持,所以我依然能保证所桥接的 IM 服务是端对端加密的。等于说,不会因为桥接而破坏了原本所使用 IM 服务的隐私保护能力。而且在部署的过程中,我发现它不光是端对端加密,也是零访问加密。
  3. 完全开源,可以自部署。不光是 Matrix 服务本身,Matrix 的所有的桥接组件也是开源的。虽然端对端加密在一定程度上已经足够了,但是只有我完全自部署,才能确定它真正是做到了零访问加密,很大程度上防止中间人攻击。

行动起来,开始部署。

部署 Matrix 服务

Matrix 服务端的各种开源实现少说也有 6 种,我最终决定使用 Synapse,因为这是唯一一款由 Matrix 核心团队自己实现的服务端。

Welcome to the documentation repository for Synapse, a Matrix homeserver implementation developed by the matrix.org core team.

Introduction to Synapse @ https://github.com/matrix-org/synapse

为了方便管理和后续维护,我选用了官方文档上面通过 Docker 的部署方式。文档写的真心完善,但是要顺利部署好,还是要踩几个坑的。

在开始部署之前,首先要确定好给 Matrix 服务用的域名,这边我们就先以 homeserver.mymatrixhost.com 吧。确定好域名就开始折腾配置了,至于怎么安装 Docker 我就不在这里赘述了。

Matrix 配置

首先是用如下命令生成配置:

docker run -it --rm \
    --mount type=volume,src=synapse-data,dst=/data \
    -e SYNAPSE_SERVER_NAME=homeserver.mymatrixhost.com \
    -e SYNAPSE_REPORT_STATS=no \
    matrixdotorg/synapse:latest generateCode language: Bash (bash)
  • SYNAPSE_SERVER_NAME (必须): 服务使用的域名,就是我们刚刚假定的 homeserver.mymatrixhost.com。
  • SYNAPSE_REPORT_STATS (必须): 是否上报匿名统计。我选择 no。
  • /data: Synapse 默认的配置文件夹路径,所以上面的范例命令,我们将卷挂载在了 /data

如果我们选择数据源目录为 synapse-data,执行完这条命令后,在 Docker 常规配置下,就能在 /var/lib/docker/volumes/synapse-data/_data 这个路径下面发现这个文件 homeserver.yaml,这个就是 Matrix 服务端的配置文件,如下:

# Configuration file for Synapse.
#
# This is a YAML file: see [1] for a quick introduction. Note in particular
# that *indentation is important*: all the elements of a list or dictionary
# should have the same indentation.
#
# [1] https://docs.ansible.com/ansible/latest/reference_appendices/YAMLSyntax.html
#
# For more information on how to configure Synapse, including a complete accounting of
# each option, go to docs/usage/configuration/config_documentation.md or
# https://matrix-org.github.io/synapse/latest/usage/configuration/config_documentation.html
server_name: "homeserver.mymatrixhost.com"
pid_file: /data/homeserver.pid
listeners:
  - port: 8008
    tls: false
    type: http
    x_forwarded: true
    resources:
      - names: [client, federation]
        compress: false
database:
  name: sqlite3
  args:
    database: /data/homeserver.db
log_config: "/data/homeserver.mymatrixhost.com.log.config"
media_store_path: /data/media_store
registration_shared_secret: "bzxSPVVwhHMiSs6b6YRBKreN-i^W^2tCUmS^4r~Hr:_ew,Alkb"
report_stats: false
macaroon_secret_key: "J1kYVZ~+fixo*RI@K5,~W-LoL#lMr0ZVJg.nFN,=MT_bYpk@JJ"
form_secret: "2zXL_q~hI1nF^m#yBumaIvY9dFU~j9uiFO0bGR5Rgc-U5gf6@2"
signing_key_path: "/data/homeserver.mymatrixhost.com.signing.key"
trusted_key_servers:
  - server_name: "matrix.org"

# vim:ft=yamlCode language: YAML (yaml)

大多数配置项可以直接是用默认,简单解释一下几个配置项:

监听器

listeners:
  - port: 8008
    tls: false
    type: httpCode language: YAML (yaml)

如果需要开启 https 的话,需要将 tls 改成 true。但是我非常不建议,原因有三:

  1. 为容器中应用直接配置证书相对比较麻烦,
  2. 保留 http ,测试起来会方便很多
  3. 可以用一些替代方案来保留 http 并同时以 https 对外提供服务,比方说,文档中提到了使用反向代理,这也是我使用的方案。同时考虑到 http 的安全性问题,我通过防火墙关闭了外部对 8008 端口的访问,因此,只有我在主机中调试的时候,能本地调用 8008 的 http 服务。

数据库

database:
  name: sqlite3
  args:
    database: /data/homeserver.dbCode language: YAML (yaml)

Synapse 支持 PostgreSQL 和 SQLite,考虑到我只是拿来自用以及更方便的部署,我选择了 SQLite。同时因为配置文件夹使用了默认的 /data ,所以最终配置文件路径为 /data/homeserver.db

接下来就是

运行 Matrix

因为后面除了 Synapse我们还需要运行桥接组件而这些桥接组件将会以独立容器运行并不会 Synapse 在一个容器里面所以非常有必要在运行 Matrix 之前把网络桥先配置好这样子这些服务就能以容器名作为域名来互相进行网络访问了

docker network create synapse-netCode language: Bash (bash)

现在可以尝试运行 Matrix 了。

docker run -d --name synapse \
    --mount type=volume,src=synapse-data,dst=/data \
    -p 8008:8008 \
    --net synapse-net \
    matrixdotorg/synapse:latestCode language: Bash (bash)

如上命令,我们通过 –name synapse 来命名 Matrix 服务的容器,以便于我们后面通过这个容器名来对容器进行操作。我们也通过 --net synapse-net 将这个容器链接到了 synapse- net 这个网络桥,这样子别的容器可以通过 synapse 这个域名来访问这个容器。

docker logs --tail 100 synapseCode language: Bash (bash)

可以通过这个命令查看下最新 100 条日志,检查一下运行情况。如果只是自用没有过多复杂配置的话,让 Matrix 跑起来其实非常容易。在用防火墙堵住 8008 端口之前,可以访问这个地址 http://{你所部署服务主机的对外 IP}/_matrix/static/ 检查你的 Matrix 状态。如果正常的话,会显示如下:

同时可以通过这个命令 docker network inspect synpase-net 查看所以连接到 synpase-net 网络桥的容器的网络配置。

HTTPS 配置 :反向代理

作为一个懒人,我的环境是通过 DigitalOcean 的一键部署功能创建的 LAMP 主机,所以虽然我对 Apache2 并不熟悉,但是为了图方便,还是选择用 Apache2 来设置反向代理了。

首先先在 /etc/apache2/sites-available/000-default-le-ssl.conf 增加如下配置:

<VirtualHost *:443>
    ServerName homeserver.mymatrixhost.com

    RequestHeader set "X-Forwarded-Proto" expr=%{REQUEST_SCHEME}
    AllowEncodedSlashes NoDecode

    ProxyPreserveHost on
    ProxyPass /_matrix http://127.0.0.1:8008/_matrix nocanon
    ProxyPassReverse /_matrix http://127.0.0.1:8008/_matrix
    ProxyPass /_synapse/client http://127.0.0.1:8008/_synapse/client nocanon
    ProxyPassReverse /_synapse/client http://127.0.0.1:8008/_synapse/client

    ErrorLog ${APACHE_LOG_DIR}/error-matrix-server.log
    CustomLog ${APACHE_LOG_DIR}/access-matrix-server.log combined

    Include /etc/letsencrypt/options-ssl-apache.conf
    SSLCertificateFile /etc/letsencrypt/live/homeserver.mymatrixhost.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/homeserver.mymatrixhost.com/privkey.pem
</VirtualHost>Code language: Apache (apache)

在真正生效以上配置之前,还需要启用必要的 Apache 模块。可通过如下命令启用:

a2enmod headers
a2enmod proxy
a2enmod proxy_httpCode language: Bash (bash)

其实在配置反向代理的过程中,我遇到了一个比较奇怪的事情。当我没有启用 headers 或者 proxy 模块的情况下,我直接重启 Apache2 服务,Apache2 会启动失败并报错提示我启用这两个模块。但是如果 proxy_http 没有启用的话, Apache2 服务是可以被正常启动的。只是在通过代理端口访问服务的时候,会报如下错误:

AH01144: No protocol handler was valid for the URL /_matrix/static (scheme 'http'). If you are using a DSO version of mod_proxy, make sure the proxy submodules are included in the configuration using LoadModule.Code language: JavaScript (javascript)

报错内容也让我很费解,里面提到的依然是 mod_proxy,直到我在 stackoverflow 一个高赞回答的评论里面发现除了 proxy ,proxy_http 也需要被启用。

不是很了解 Apache2,哪位朋友明白为什么报错什么费解的话,求解答。

接下来就是申请 https 证书和绑定域名,这里就不再赘述了。一旦配置成功,当你访问 https://homeserver.mymatrixhost.com/_matrix/static/ ,能再次看到如下页面。

后面我们将 Matrix 用于生产使用,所以务必关闭外部对 8008 端口的访问能力,因为 8008 提供的是不安全的 HTTP 服务。

客户端验证

既然 HTTPS 已经配置好,那我们可以正式使用 Matrix 服务了。再使用之前,还需要完成两件事情:管理员账户的创建和客户端。

创建管理员账号

可以通过如下命令来创建一个管理员账户

docker exec -it synapse register_new_matrix_user http://localhost:8008 -c /data/homeserver.yaml -aCode language: Bash (bash)

务必要加上 -a 参数,否则创建的将会是普通账户。在此,我们假定这个管理员账户为 matrixadmin 以便后面使用。

使用客户端连接 Matrix

因为 Matrix 是开源协议,所以可以从该页面发现,现在业界已经有很多 Matrix 客户端。在我尝试了 ElementFluffyChat 之后,我最终选择了 FluffyChat。

选定好客户端后,就可以尝试登陆了。登陆时,务必将服务端从 Matrix.org 改成我们自部署的 homeserver.mymatrixhost.com,并输入上一步中生成的管理员账户的用户名和密码。不出意外的话,就可以顺利通过客户端登陆 Matrix 服务了。

接下来,将开始进入最有意思的部分:将这个 Matrix 服务变成一个多合一端对端加密 IM 服务。

部署桥接组件

先从 WhatsApp 和 Telegram 目前最流行的两大 IM 开始吧。和部署 Synapse 一样,我参考官方文档同样选择使用 Docker 来运行桥接组件

部署 WhatsApp 桥接组件

WhatsApp 配置

先使用此命令 mkdir mautrix-whatsapp && cd mautrix-whatsapp 创建一个用来维护配置的目录,接下来执行 docker run --rm -v `pwd`:/data:z dock.mau.dev/mautrix/whatsapp:latest 以初始化配置。然后,你会在 mautrix-whatsapp 目录中看到 config.yaml 这个文件。

大多数配置可以直接使用默认值,简单介绍一些需要修改和关注的配置项。

homeserver:
    # The address that this appservice can use to connect to the homeserver.
    address: http://synapse:8008
    # The domain of the homeserver (also known as server_name, used for MXIDs, etc).
    domain: homeserver.mymatrixhost.com

    # What software is the homeserver running?
    # Standard Matrix homeservers like Synapse, Dendrite and Conduit should just use "standard" here.
    software: standard
    # The URL to push real-time bridge status to.
    # If set, the bridge will make POST requests to this URL whenever a user's whatsapp connection state changes.
    # The bridge will use the appservice as_token to authorize requests.
    status_endpoint: null
    # Endpoint for reporting per-message status.
    message_send_checkpoint_endpoint: null
    # Does the homeserver support https://github.com/matrix-org/matrix-spec-proposals/pull/2246?
    async_media: false

# Application service host/registration related details.
# Changing these values requires regeneration of the registration.
appservice:
    # The address that the homeserver can use to connect to this appservice.
    address: http://mautrix-whatsapp:29318Code language: YAML (yaml)

不知道是否还记得,我们前面创建了 synapsenet 这个网络桥,这样子容器间可以将容器名作为域名来互相进行网络通讯。以上 homeserver 的 address 配置项正是用来声明前面我们部署的 Synapse 服务。由于这些容器都运行在同一个主机,WhatsApp 桥接组件可以通过 8008 端口来访问 Synapse 的非安全 http 服务,所以我们将 address 配置为 http://synapse:8008。以此类推,appservice 的 address 配置项为 http://mautrix-whatsapp:29318。domain 当然就是之前我们假定的 homeserver.mymatrixhost.com。

    # Database config.
    database:
        # The database type. "sqlite3-fk-wal" and "postgres" are supported.
        type: sqlite3-fk-wal
        # The database URI.
        #   SQLite: A raw file path is supported, but `file:<path>?_txlock=immediate` is recommended.
        #           https://github.com/mattn/go-sqlite3#connection-string
        #   Postgres: Connection string. For example, postgres://user:password@host/database?sslmode=disable
        #             To connect via Unix socket, use something like postgres:///dbname?host=/var/run/postgresql
        uri: file:///data/db.db?_txlock=immediateCode language: YAML (yaml)

一样的道理。虽然 WhatsApp 桥接组件也支持 PostgreSQL,但自用服务且图方便,我选择 SQLite。如果启动的时候,遇到初始化 DB 失败的报错,可以直接用 touch db.db 来创建一个空数据库文件。

    # End-to-bridge encryption support options.
    #
    # See https://docs.mau.fi/bridges/general/end-to-bridge-encryption.html for more info.
    encryption:
        # Allow encryption, work in group chat rooms with e2ee enabled
        allow: true
        # Default to encryption, force-enable encryption in all portals the bridge creates
        # This will cause the bridge bot to be in private chats for the encryption to work properly.
        default: trueCode language: YAML (yaml)

将 allow 设置为 true,WhatsApp 桥接组件将会支持对桥接会话开启端对端加密。将 default 设置为 true 之后,该桥接组件就会默认对所有的桥接会话开启端对端加密。

permissions:
    "*": relay
    "homeserver.mymatrixhost.com": user
    "@matrixadmin:homeserver.mymatrixhost.com": adminCode language: YAML (yaml)

最后就是桥接应用服务的权限配置,admin 就是之前我们假定的 matrixadmin 用户,如上。

注册应用服务

配置文件已确定好,接下来就该生成应用服务注册文件。再次执行刚刚相同的命令。

docker run --rm -v `pwd`:/data:z dock.mau.dev/mautrix/whatsapp:latestCode language: Bash (bash)

配置文件夹中会出现一个新文件 registration.yaml,然后将该文件拷贝到 Synapse 的配置文件夹中,并起一个更容易识别的文件名。

cp registration.yaml /var/lib/docker/volumes/synapse-data/_data/mautrix-whatsapp-registration.yamlCode language: Bash (bash)

接下来更新 Synapse 配置文件将 WhatsApp 桥接组件注册到 Matrix 中,如下:

trusted_key_servers:
  - server_name: "matrix.org"
app_service_config_files:
  - /data/mautrix-telegram-registration.yaml
  - /data/mautrix-whatsapp-registration.yaml

# vim:ft=yamlCode language: YAML (yaml)

从上面可以看到另外一个和 Telegram 相关的服务注册,一会就会介绍。

开启 WhatsApp 桥接服务

接下来启动桥接服务。前面我们将 appservice 的 address 配置为 http://mautrix-whatsapp:29318,所以我们务必将容器命名为 mautrix-whatsapp 并关联到 synapsenet,如下:

docker run --restart unless-stopped --name mautrix-whatsapp --net synapse-net -v `pwd`:/data:z dock.mau.dev/mautrix/whatsapp:latestCode language: Bash (bash)

使用 Matrix 桥接 WhatsApp 会话

开启桥接服务后,重启 Synapse,不出意外的话 WhatsApp 桥接就正式工作了。可以在客户端添加 @whatsappbot:homeserver.mymatrixhost.com 为好友,向他发送 login,whatsappbot 就会进行桥接的登陆引导,整个流程和登陆网页版 WhatsApp 一模一样。我理解这个桥接组件其实就是一个网页版 WhatsApp 的封装。

至此,WhatsApp 桥接服务算是正常运行了,接下来再聊聊 Telegram 的桥接吧。

部署 Telegram 桥接组件

Telegram 配置

部署和 WhatsApp 桥接组件类似,创建配置目录并生成配置文件

mkdir mautrix-telegram && cd mautrix-telegram.
docker run --rm -v `pwd`:/data:z dock.mau.dev/mautrix/telegram:latestCode language: Bash (bash)

同样需要注意 Matrix 和 Telegram 桥接应用服务的地址

# Homeserver details
homeserver:
    # The address that this appservice can use to connect to the homeserver.
    address: http://synapse:8008
    # The domain of the homeserver (for MXIDs, etc).
    domain: homeserver.mymatrixhost.com
    # Whether or not to verify the SSL certificate of the homeserver.
    # Only applies if address starts with https://
    verify_ssl: false
    # What software is the homeserver running?
    # Standard Matrix homeservers like Synapse, Dendrite and Conduit should just use "standard" here.
    software: standard
    # Number of retries for all HTTP requests if the homeserver isn't reachable.
    http_retry_count: 4
    # The URL to push real-time bridge status to.
    # If set, the bridge will make POST requests to this URL whenever a user's Telegram connection state changes.
    # The bridge will use the appservice as_token to authorize requests.
    status_endpoint:
    # Endpoint for reporting per-message status.
    message_send_checkpoint_endpoint:
    # Whether asynchronous uploads via MSC2246 should be enabled for media.
    # Requires a media repo that supports MSC2246.
    async_media: false

# Application service host/registration related details
# Changing these values requires regeneration of the registration.
appservice:
    # The address that the homeserver can use to connect to this appservice.
    address: http://mautrix-telegram:29317Code language: YAML (yaml)

同样选择 SQLite 作为数据库

    # The full URI to the database. SQLite and Postgres are supported.
    # Format examples:
    #   SQLite:   sqlite:///filename.db
    #   Postgres: postgres://username:password@hostname/dbname
    database: sqlite://db.dbCode language: YAML (yaml)

端对端加密配置

    # End-to-bridge encryption support options.
    #
    # See https://docs.mau.fi/bridges/general/end-to-bridge-encryption.html for more info.
    encryption:
        # Allow encryption, work in group chat rooms with e2ee enabled
        allow: true
        # Default to encryption, force-enable encryption in all portals the bridge creates
        # This will cause the bridge bot to be in private chats for the encryption to work properly.
        default: trueCode language: YAML (yaml)

权限配置

    permissions:
        '*': relaybot
        mymatrixhost.com: full
        '@matrixadmin:homeserver.mymatrixhost.com': adminCode language: YAML (yaml)

除此之外,还需要配置 Telegram 的 API Key。我相信 Telegram 桥接应用服务是对一系列 Telegram API 操作的封装。

telegram:
    # Get your own API keys at https://my.telegram.org/apps
    api_id: 10000001
    api_hash: 123abcdefghijklmnopqrstuvwxyz123
    # (Optional) Create your own bot at https://t.me/BotFather
    bot_token: disabledCode language: PHP (php)

显而易见,上面的 Key 是我虚构的,只是作为例子而已。我们需要去 Telegram 应用创建页面申请 API Key。申请过程中,务必在 URL 输入你 Matrix 服务的域名,这里我们就需要输入我们假定的 homeserver.mymatrixhost.com。

注册应用服务

和 WhatsApp 桥接组件的步骤类似,

docker run --rm -v `pwd`:/data:z dock.mau.dev/mautrix/telegram:latest
cp registration.yaml /var/lib/docker/volumes/synapse-data/_data/mautrix-telegram-registration.yamlCode language: Bash (bash)

更新 Synapse 配置文件,如下:

trusted_key_servers:
  - server_name: "matrix.org"
app_service_config_files:
  - /data/mautrix-telegram-registration.yaml
  - /data/mautrix-whatsapp-registration.yaml

# vim:ft=yamlCode language: YAML (yaml)

开启 Telegram 桥接服务

接下来启动桥接服务。前面我们将 appservice 的 address 配置为 http://mautrix-telegram:29317,所以我们务必将容器命名为 mautrix-telegram 并关联到 synapsenet,如下:

docker run --restart unless-stopped --name mautrix-telegram --net synapse-net -v `pwd`:/data:z dock.mau.dev/mautrix/telegram:latestCode language: Bash (bash)

使用 Matrix 桥接 Telegram 会话

开启桥接服务后,重启 Synapse。可以在客户端添加 @telegrambot:homeserver.mymatrixhost.com 为好友,向他发送 login,telegrambot 就会进行桥接的登陆引导,整个流程和登陆 Telegram 一模一样。

至此,Telegram 桥接服务开始正常运行,我们可以用 FluffyChat 收发 Telegram 的会话信息。

多合一端对端 IM 服务

在适应了两天之后,我将 WhatsApp 和 Telegram 的消息通知都关闭了,FluffyChat 配合我自部署的 Matrix 已经完全可以搞定我日常的 IM 需求了。

最后,不要忘记用防火墙关闭外部对 29317 和 29318 两个桥接服务端口的访问。

我的物理全键盘手机情怀

最近入了一款很小众的全键盘手机 Unihertz Titan Slim,再一次唤醒了我对全键盘手机的热爱。突然有了写一篇日志来记录下我这份情况的想法。

印象中,应该是 2008 年,我还在攻读硕士学位的时候。那个时候我是彻彻底底的微软粉,正用着我人生的第一部触摸屏手机,操作系统是 Windows Mobile 系统。有事没事刷个机,有时候还会用 C# 手搓一个自己需要的手机软件(那个年代,我们还不管手机应用软件叫 App),非常有意思。但是也不得不承认,那个时候触摸屏的体验很一般。而且手机系统的交互对触摸屏的支持还没有进化到很完美,大多数操作都是要通过一只触摸笔。

Samsung i718

我的同班同学 Solrex 给我介绍了他新入的黑莓手机,虽然黑莓手机和当时很多主流手机一样也是非触摸屏,但是它的系统配合侧面的可按压滚轮,使得交互异常顺滑,瞬间吸引了我。于是,我也入手了一款二手黑莓 8700。据我了解, 2008 年在中国境内只能买到翻修过的二手黑莓手机。

黑莓 8700

这也是我拥有的第一款物理全键盘手机。

Blackberry 8700

在笔画和九宫格拼音输入法大行其道的时候,黑莓的全键盘简直是一股清流涌入手机市场。全键盘带来的爽快感简直是数字九宫格所无法比拟的。同时它特有的可按压滚轮带来的顺滑感也完全碾压业界最流行的上下键配合确定键。当时黑莓主打的是商务领域,不得不承认它在硬件交互上的确下了不少功夫,把整体操作效率提升到了一个新的高度。

其实很长的一段时间,黑莓都是一个着眼于商务的小众品牌,直到美国前总统奥巴马的一个固执决定,让它进入了大众视野。理论上来说,当奥巴马入住白宫之后,需要将自用手机替换为总统的专用手机,但是奥巴马却要求保留它的黑莓手机。而这个有意思的决定,使得黑莓上了各版头条。

随着 Android 和 iPhone 的越发强势,各类主流 App 对 BlackBerry OS 的支持变得越来越糟糕,所以在我的黑莓 8700G 罢工之后,我有一段时间并没有再使用物理全键盘手机,但我依然在等待黑莓能够出品一款眼前一亮的产品。最后终于我等到了。

BlackBerry Passport

BlackBerry Passport 为什么叫 Passport,是因为它的尺寸和护照几乎一模一样。

Size of Blackberry Passport

它的正方形屏幕、质感等特有的设计语言深深的吸引了我,而且它独创的为全键盘增加了触摸设计,使得你可以通过在键盘上滑动手指对菜单、网页浏览等实现滚轮翻页效果,非常惊艳。

用键盘触摸板滑动光标

而且 BlackBerry 搭载的 BlackBerry OS 10 兼容 Android 应用,虽然只支持 Android 2.3 以下的多数应用,但在当时已经可以满足日常需求了,这也是我又重新选择黑莓物理全键盘手机的重要原因之一。

当时为了购买这款手机,我首次尝试了海淘。从美国官网购买后发货到香港,然后又委托了在香港的朋友帮我寄到了中国大陆。

整体使用上来说,除了因为正方形屏幕带来的 UI 适配问题,其他体验都不错 😄

黑莓手机的终结

从 2016 年开始,黑莓公司就停止对手机硬件的开发,开始授权第三方公司发布手机。虽然黑莓不开发硬件,但是依然参与设计,所以即便授权给了第三方公司,TCL 发布的几款黑莓手机还不错的,比如 BlackBerry KEYone。然后在 2020 年 TCL 也宣布不再设计和发布任何黑莓手机,并且黑莓公司于 2022 年停止了对所有黑莓手机的服务,正式宣告了黑莓手机的终结。

物理全键盘手机情怀党的福音

虽然黑莓手机已经退出历史舞台,但是依然有一些小众公司在延续这份情怀,比如 Unihertz。这个公司发布的手机都特别小众和有特点,例如:支持对讲机功能的 Atom XL,堪称世界上最小的 4G 手机 Jelly 系列,超长续航带有 22000 mAH 的 Tank,当然也包括带有物理全键盘复刻黑莓手机的 Titan 系列。

情怀从 Titan 延续

Unihertz Titan

出于对 BlackBerry Passport 的钟爱,Titan 一下子就抓住了我的眼球。虽然它和 Passport 差的很远,但是 Titan 的一些小设计,让我感受到了 Unihertz 对黑莓的一些特性和痛点的确是懂的。

正方形屏幕的困扰

使用 BlackBerry Passport 遇到最多的问题,就是应用对正方形屏幕的兼容问题,而 Titan 手机可以通过三指快速向下滑动切换至迷你模式来改变屏幕展示区域的比例,非常方便。

激活迷你模式

可圈可点的继承和优化

同时它也继承了 Passport 的一些优点,比如:你同样可以将键盘当作触摸板来进行翻页,手感不错的全键盘。同时针对全键盘定制化的 Kika 输入法。

太重了!

Titan 不管是作为 Passport 的复刻,还是一款三防手机,它都是合格的。但是它真的是太重了,有时候玩久一会甚至会让手腕有酸痛感,倒是大幅减少我看手机的时间。同时,随着现在应用越发复杂和高度功能集成,正方形屏幕的兼容能力开始力不从心了。

高频的迷你模式切换和过重的份量,使得我一直在期待 Unihertz 能出一个修长且相对轻便的物理全键盘手机。终于我等到了 Titan Slim

Titan Slim

Unihertz Titan Slim

上图,左侧是 Titan,中间是 Titan Slim,右侧是 Titan Pocket。Titan Pocket 发布于 Titan 之后和 Titan Slim 之前,虽然轻便了很多,但是依然是正方形屏幕,所以我没有考虑。

Titan Slim 硬件方面和软件性能相比 Titan 做了一定的提升和优化,但整体上并没有本质变化。在交互上,也继承了 Titan 的特性,例如:键盘触摸、物理全键盘等等。设计上却有了很大变化,长方形屏幕对当代 App 有了更好的兼容;可能是因为物理全键盘的缘故,所以手机依然相对偏厚,但是相对于 Titan 已经轻了很多。长时间用,手腕也没有明显的酸疼感。目前 Titan Slim 正是我的主力机。

情怀可否继续延续

其实延续黑莓情怀,并不是一件易事。现在虽然如 Unihertz 的小众企业在复刻黑莓手机,但是不得不承认这些复刻都谈不上完美或者说不够让人满意。在坚持使用这些物理全键盘手机的时候,必然需要做一些妥协。比如我自己,三星手机是我非物理全键盘手机中的最爱,所以之前,我不得不为了三星手机暂时告别物理全键盘手机,而现在我又因为 Titan Slim 暂时告别了三星手机。可能稍微能让我欣慰的是,我平时都是携带两部手机,一部主手机,一部工作手机,但其实工作手机的使用量很小,大多数情况下还是用工作电脑。

我也一直在期待,有一块更完美的物理全键盘手机可以出现。让我不再需要做出妥协的选择。

reMarkable 2:一个可玩性很强的新玩具

趁着生日,又赶上黑五,终于顺利批下预算入了一个新玩具: reMarkable 2。😂

reMarkable 2
reMarkable 2

在入之前,就好几个哥们向我推荐过这个设备了。最终说服我自己购买的,不外乎是,它极致的纸质书写手感、轻薄的设计等等卖点。但当我用了 reMarkable 2 一段时间之后,发现它的实用性和可玩性远超我的想象。最让我出乎意料的一点是,reMarkable 的系统是基于 Linux 的,而且制造商还慷慨地开放了 SSH 和提供了 root 密码。

刚好赶上年底休假,我也把这玩意好好把玩了一下,顺便水一篇文章来当作笔记了。

功能完备性

语言问题

reMarkable 2 对于非拉丁文字的支持非常的糟糕,中日韩文字都会显示为方块,如下图。

方块
方块

有个折中的解法是尽量阅读 PDF,但是我们也不能保证我们所阅读的 PDF 文档一定内嵌了所有必要字体。如果一些中日韩 CJK (Chinese, Japanese, and Korean) 字符依赖未内嵌的字体,在 reMarkable 上面还是会显示成方块。

好在 reMarkable 操作系统是基于 Linux,且开放 SSH 并提供了 root 密码,我们可以很方便的安装中日韩 CJK (Chinese, Japanese, and Korean) 字体。可以参考这个页面提供的 RMPKG 来安装。

cd /tmp;
wget https://files.davisr.me/projects/rcu/download-fontrmpkg/NotoSansCSFont.rmpkg
wget https://files.davisr.me/projects/rcu/download-fontrmpkg/NotoSansCTFont.rmpkg
wget https://files.davisr.me/projects/rcu/download-fontrmpkg/NotoSansJPFont.rmpkg
wget https://files.davisr.me/projects/rcu/download-fontrmpkg/NotoSansKRFont.rmpkg
chmod +x *.rmpkg
./NotoSansCSFont.rmpkg --install
./NotoSansCTFont.rmpkg --install
./NotoSansJPFont.rmpkg --install
./NotoSansKRFont.rmpkg --install
rm -f *.rmpkgCode language: Bash (bash)

这样子,reMarkable 内嵌的 Noto Sans 字体就会支持 CJK 字符的显示。

类型多样化

reMarkable 2 原生支持的文档格式只有 PDF 和 EPUB,这显然是远远不够的。Toltec 这个社区维护的针对 reMarkable 的免费软件源可以帮助到我。通过 Toltec 我们可以在 reMarkable 2 KOReader。依托于 KOReader 强大的格式兼容性,我们就可以用 reMarkable 阅读几乎所有主流格式的文档和书籍了。

## 安装完 Toltec 之后,通过 opkg 命令来安装 remux 和 KOReader
opkg update
opkg upgrade
# remux
# 支持多任务的启动器
opkg install remux
# KOReader
# 支持 PDF、DjVu、EPUB、FB2 等多种格式的电子书阅读器
opkg install koreaderCode language: Bash (bash)

值得一提的是,reMarkable 的原生书架和 KOReader 的书架不能共享,当然我们可以用一些云存储来解决,后面会详细介绍。另外一定要再三确认,Toltec 是否支持你所持有 reMarkable 的系统版本。

Toltec 里面还有很多有意思的应用,有兴趣的可以慢慢研究。

文章及文档管理

reMarkable 提供了一个浏览器插件。这个插件可以让用户点击一下就能将网页转换成 EPUB 文章并放到 reMarkable 的书架上。虽然方便,但是我用起来不怎么习惯:

  1. 插件转换成的 EPUB 内容的格式非常奇怪。
  2. Pocket 是我一直在使用的稍后阅读应用,我并不打算切到 reMarkable。另外我也不希望把所有收藏的页面都放在 reMarkable 书架上面。

于是我着手尝试将 Pocket 收藏的页面自动推送到 reMarkable ,又于是我想起了 n8n 这个开源的工作流自动化平台。

之前对 n8n 只是略有耳闻,这次尝试了一下,的确很方便。我将 n8n 部署在 NAS 上面,然后轻松完成了工作流的搭建,下图 n8n 对 Workflow 的展示和编辑 UI,非常的直观和友好。

将 Pocket 收藏上传至 Google Drive
将 Pocket 收藏上传至 Google Drive

我通过调度器定时运行如上图工作流:

  1. 读取上一次检测的时间点,从 Pocket 读取该时间之后我所收藏的网址列表。Pocket 的获取 API 有一个 since 参数,通过这个参数我们可以排除掉所有这个时间点之前的网址,以避免重复拉取。
  2. 如果列表不为空,会使用 OneSimpleAPI 将这些页面依次转换成 PDF 文档并上传至 Google Drive
  3. 把检测时间点更新为当前。

以上的 OneSimpleAPI 和 Google Drive 的工作流节点都是 n8n 原生支持的,调用起来非常方便。

在创建上面这个工作流的时候,有几点是需要注意:

  1. 我发现 Pocket 的 since 参数是以最后更新时间来作为参照物的,所以你如果希望把一些旧的页面推送至 Google Drive,可以将其做一些修改,例如:修改一下标签、重新收藏一下这个页面。下次被调度的时候,这个页面就会被推送至 Google Drive。
  2. 当你在 Pocket 中删除了一个收藏,Pocket 在彻底删除这个收藏之前,会先将这个收藏的状态改成为 2。所以在转换时需要过滤掉状态为 2 的收藏,以免将已删除的收藏被上传了。
  3. n8n 的 Google Drive 库支持两个认证方式,分别是 OAuth 2 和服务账号。OAuth 2 要求你的回调地址是使用域名的。出于安全考虑,我的 NAS 并没有开放外部直接访问,是需要 VPN 的,因此也没有绑定顶级域名。如果各位和我情况类似的话,请直接考虑使用服务账号。务必将用来上传文件的 Google Drive 目录已读写权限共享给服务账号,毕竟服务账号大概率并不是你的 Gmail。
授权给服务账号的权限
授权给服务账号的权限

其实我完全可以直接将文章直接放入 reMarkable 的书架,但我个人觉得这并不是一个好主意。我还是希望通过限制 reMarkable 书架上的数量,来保证我能够完成当面的阅读后,再去 Google Drive 选择新的读物。我不想让过多的文章来毁掉我的 reMarkable 书架。

我之前也提到 KOReader 和 reMarkable 2 不能共享书架,所以将文档和书籍放在 Google Drive 的中间云存储是一个既能保证书架有条理,也能保证两方都能方便拉取文档的最适合折中方案。

出版物管理

长久以来,我一直是使用 Calibre 来管理各种出版物,例如:小说、书籍、漫画和论文等等。后来有了 NAS,我又将 Calibre 的书库迁移到了 NAS 中,并在 Docker 容器中运行 Calibre Web,这样子我在任何设备都能对我的出版物收藏进行管理。

Calibre Web 其实并非是 Calibre 的官方出品,在 GitHub 也有不止一个 fork。我个人比较推荐 linuxserver.io 的镜像。假如你在部署 Calibre Web 之前没有使用过 Calibre,那你会需要一个初始的书库文件 metadata.db 。你可以安装 Calibre 来初始化一个,或者直接从这里下载。

Calibre Web 除了给用户提供了对 Calibre 的书库基于 Web 的管理能力,还支持 OPDS 目录。

The Open Publication Distribution System (OPDS) Catalog format is a syndication format for electronic
publications based on Atom and HTTP. OPDS Catalogs enable the aggregation, distribution, discovery,
and acquisition of electronic publications.

OPDS Catalogs use existing or emergent open standards and conventions, with a priority on simplicity.

OPDS Catalog 1.2

这样子,我们就可以通过如下方法来搜索出版物:

# curl --user {username}:{password} "http://{IP}:{port}/opds/search/{queryString}"
curl --user username:password "http://{IP of NAS}:8083/opds/search/root%20cause"Code language: Bash (bash)

如能搜索到,ODPS 将会把出版物信息以 XML 格式返回,其中包括书名和下载链接:

<?xml version="1.0" encoding="UTF-8"?>
<feed
  xmlns="http://www.w3.org/2005/Atom"
  xmlns:dc="http://purl.org/dc/terms/"
  xmlns:dcterms="http://purl.org/dc/terms/">
  <id>urn:uuid:2853dacf-ed79-42f5-8e8a-a7bb3d1ae6a2</id>
  <updated>2022-12-29T16:23:55+00:00</updated>
  <link rel="self"
        href="/opds/search/root cause?"
        type="application/atom+xml;profile=opds-catalog;type=feed;kind=navigation"/>
  <link rel="start"
        href="/opds"
        type="application/atom+xml;profile=opds-catalog;type=feed;kind=navigation"/>
  <link rel="up"
        href="/opds"
        type="application/atom+xml;profile=opds-catalog;type=feed;kind=navigation"/>
  <link rel="search"
      href="/opds/osd"
      type="application/opensearchdescription+xml"/>
  <link type="application/atom+xml" rel="search" title="Search" href="/opds/search/{searchTerms}" />
  <title>Calibre-Web</title>
  <author>
    <name>Calibre-Web</name>
    <uri>https://github.com/janeczku/calibre-web</uri>
  </author>
  <entry>
    <title>Root Cause Analysis Handbook</title>
    <id>urn:uuid:06a66a9a-59d4-415b-a472-a413b8cf923d</id>
    <updated>2022-12-22T07:56:20+00:00</updated>
    <author>
      <name>ABS Consulting</name>
    </author>
    <publisher>
      <name>Rothstein Publishing</name>
    </publisher>
    <dcterms:language>eng</dcterms:language>
    <category scheme="http://www.bisg.org/standards/bisac_subject/index.html"
              term="Business &amp; Economics"
              label="Business &amp; Economics"/>
    <summary>Are you trying to improve performance, but find that the same problems keep getting in the way? Safety, health, environmental quality, reliability, production, and security are at stake. You need the long-term planning that will keep the same issues from recurring. Root Cause Analysis Handbook: A Guide to Effective Incident Investigation is a powerful tool that gives you a detailed step-by-step process for learning from experience. Reach for this handbook any time you need field-tested advice for investigating, categorizing, reporting and trending, and ultimately eliminating the root causes of incidents. It includes step-by-step instructions, checklists, and forms for performing an analysis and enables users to effectively incorporate the methodology and apply it to a variety of situations. Using the structured techniques in the Root Cause Analysis Handbook, you will: Understand why root causes are important. Identify and define inherent problems. Collect data for problem-solving. Analyze data for root causes. Generate practical recommendations. The third edition of this global classic is the most comprehensive, all-in-one package of book, downloadable resources, color-coded RCA map, and licensed access to online resources currently available for Root Cause Analysis (RCA). Called by users &#34;the best resource on the subject&#34; and &#34;in a league of its own.&#34; Based on globally successful, proprietary methodology developed by ABS Consulting, an international firm with 50 years&#39; experience in 35 countries. Root Cause Analysis Handbook is widely used in corporate training programs and college courses all over the world. If you are responsible for quality, reliability, safety, and/or risk management, you&#39;ll want this comprehensive and practical resource at your fingertips. The book has also been selected by the American Society for Quality (ASQ) and the Risk and Insurance Society (RIMS) as a &#34;must have&#34; for their members.</summary>
    <link type="image/jpeg" href="/opds/cover/30" rel="http://opds-spec.org/image"/>
    <link type="image/jpeg" href="/opds/cover/30" rel="http://opds-spec.org/image/thumbnail"/>
    <link rel="http://opds-spec.org/acquisition" href="/opds/download/30/pdf/"
          length="24812876" mtime="2022-12-22T07:56:20+00:00" type="application/pdf"/>
  </entry>
</feed>Code language: HTML, XML (xml)

有了对 OPDS 目录的支持,我创建了如下工作流来实现对出版物的搜索并将所有搜索文档都上传至 Google Drive。

将书籍从 Calibre 书库搜索出来并上传至 Google Drive
将书籍从 Calibre 书库搜索出来并上传至 Google Drive

我通过 Webhook 将搜索字符串传递给 OPDS 来搜索出版物。

其他

reMarkable 2 还有很多值得探索的地方,例如模板方面。reMarkable 2 对 PDF 格式支持的完成度惊到了我,尤其是对 PDF 内链的支持。很多电纸书,对 PDF 文档内的链接几乎是不支持的。即便是像目录这种非常必要的内链,也是通过调出目录列表来实现点击跳转,并不能直接在阅读过程中点击跳转。

我们可以通过 reCalendar 生成模板,然后用 reMarkable 2 以日月年维度来做计划和总结了。为了防止页面加载过慢,下面预览的时间跨度仅仅是两个月。大家可以根据需要在 reCalendar 定制你自己需要的特殊日子和时间跨度等等元素。

Loader Loading…
EAD Logo Taking too long?

Reload Reload document
| Open Open in new tab

关于 reMarkable 2 的一些总结和分享就先到这了。我还会继续开发这个玩具,有新的再分享。😁

吐槽设备管理和多电脑管理:无线总是比有线方便吗?

作为一个前信息类学科学生和现 IT 行业从业者,电脑一直是我生活中不可分割的一部分。同时如何方便的打理好我的电脑和其周边设备一直是个不断更新的问题。

为什么这个问题会不断更新呢?

环境决定需求

在学校的时候,我能够掌控地仅仅在宿舍里面的一个小桌子。所以那个时候,我所追求的方便就是周边外设要尽量轻薄和无线。这样子可以方便地把鼠标和键盘都塞到抽屉里面,同时不需要重新插拔线。当然不得不承认,如果忘了关了鼠标和键盘的开关,红外接收器真的很废电池 😂。那个时候还没有蓝牙,以及后面出现的早期版本蓝牙还不适用于支撑鼠标键盘的信息传输。

无线鼠标和信号接收器
无线鼠标和信号接收器

那个时候,看着我宿舍桌上那笨重的主机和荧光屏显示器,拥有一台笔记本电脑也当是一直我梦想的事情。这样子我就可以随身带着我的电脑;一旦放寒暑假,我可以方便地把它带回家 🤭。

荧光屏显示器和主机箱
荧光屏显示器和主机箱

工作之后,所使用的电脑设备已经几乎都是笔记本电脑了,而且随着苹果掀起的笔记本轻薄化风潮所造成的接口精简化,我不得不将随身携带的外设都换成了支持蓝牙的,要不然接口完全不够用。然后由于工作内容的保密性,需要工作电脑和生活电脑分离,我又不得不尽量使用支持多设备记忆的蓝牙设备,以保证我的一套外设可以在多个电脑之间方便切换。

无线一定比有线方便吗?

然而,因为目前公司的工作需要以及我的个人需要,最近我的设备管理变得前所未有的麻烦 🤦。我需要让一套外设(键盘、鼠标、摄像头、显示器)可以方便地在四台笔记本和一台主机箱之间切换,显然蓝牙已经不可能做到了。

顺便提一句,不要对五台电脑觉得奇怪,我在前前公司做涉及到移动端项目的时候,我有十几台手机、几台平板电脑和四台电脑,当然他们几乎都不属于我个人,是属于公司的。那个时候还没有云测试平台,所有兼容性测试都只能在本地设备上完成。

言归正传,好在我的大多数外设都是有线无线双模的,所以我想了折中方案。我把一套鼠标、键盘和显示器都接在一个多接口转换器上,当我需要用哪台电脑时,我就把转换器接到哪个电脑上。由于 COVID 的影响已经很小,所以我们偶尔也需要去办公楼一下。所以当我需要去公司的时候,我只需要将显示器线拔掉,连着扩展器和键鼠一起塞到背包里。搞定!

前几天,我突发奇想,既然有 HDMI 切换器这种东西来实现显示器信号源的自由切换,难道就没有一个硬件可以实现所有外设的切换吗?于是,我真的发现了 KVM 切换器,又叫多电脑切换器。这个 KVM 不是基于内核的虚拟机,它代表了 Keyboard、Video 和 Mouse。

KVM切換器(英語:KVM switch),一般簡稱KVM,又名多電腦切換器,是一種计算机硬件設備,可以使用户透過一組鍵盤螢幕滑鼠控制多台電腦。KVM,即键盘、显示器、鼠标的英文首字母缩写(Keyboard、Video、Mouse)。

摘自 维基百科
KVM 切换器 图纸
KVM 切换器 图纸

这个切换器真是拯救了我。我不需要准备多套外设来覆盖我所有的电脑,也不需要反复插拔接线来保证我的所有电脑都能被操作。我的桌面也变得更有条理了一些,虽然还是很乱。

选择合适的 KVM 切换器

KVM 切换器的种类很多,坑也很多。选购的时候,要关注技术参数,尤其是 HDMI 部分,参数不同差异很大。

  1. 首先要确定电脑数量,即,你需要将这套外设共享给几台电脑。
  2. USB 的参数。因为 USB 1 和 USB 2 的传输速率相差很大,主要看你的需求,是否有很多外置存储设备等等。
  3. HDMI 的规格。如果你使用 Apple TV 等支持 HDR 和杜比视界的设备,需要查看提供的 HDMI 是否支持 HDR 和杜比视界的传递。HDR 有很多种,例如 HDR、HDR 10、HDR 10+ 等等。同样,也要关注是否支持音频传递,以及支持的音频编码。
  4. 对于视频和游戏爱好者,支持的最高刷新率和分辨率尤为重要。需要关注一下。
  5. 是否支持热切换。直接影响切换效率和用户体验。

希望这篇吐槽能帮到一些同样苦于多设备管理的朋友。

我初期用于提升英语的方法

到新加坡一年多了,一直工作在一个几乎没人说汉语的公司。当同事通过闲聊发现我之前没有留学过也没有在任何英语环境下工作过,大多数都表示很诧异。也说明了另外一个事实:中国人哑巴英语的情况非常严重,严重到不少老外们都发现了。最近也有很多国内朋友问我怎么提升英语,虽然我的英语还是很糟糕,没留学和海外工作经验且英语比我好的人真的是太多了,而且我也不敢妄称我有能力去教别人,但是我还是决定分享一下我初期用于提升英语的方法,希望能够帮助一些想要做出一些改变的朋友入门初级英语练习 😄。

张开嘴

最初,练习英语完全是出于偶然。当时我刚刚从北京搬到杭州,在杭州我几乎没有朋友。于是我希望能够在工作伙伴外也能交到一些朋友,就决定参加英语角试试看。

我很幸运地发现了一个叫 Have Fun English Club 的英语角,结识了一些有意思的朋友。在这我衷心感谢这几位朋友,是他们给了我很多动力,帮助我熬过了人生中极其灰暗的一段日子。刚开始的时候,我是典型的轻度哑巴英语。平时说中文的时候,不带打嗝的,一切到英语就立马变得小声,而且磕磕巴巴的。不过好在我这个人脸皮够厚,很快就和大家打成一片,当时我给自己的要求是可以错,但是不能怂。🤣 为了能够更多地结交朋友,我申请成为了 HFEC 的志愿者,去担任他们活动的 Group Leader,帮助他们一起起草 Topic。久而久之,我便喜欢上了练习英语(改天我会再写一篇日志,介绍一下我们英语角的活动模式。这个模式比较独特在当地英语角圈还是挺受欢迎的)

没多久,HFEC 因为一些内部原因解散了。突然之间没有英语角可以唠嗑了,大家觉得很无聊。为了能够继续磨练自己的口语和结交更多的朋友,我们几个在 HFEC 认识的老朋友就创立了 Whatever English Club,模式更多的是借鉴 HFEC 的模式,但增加了更多游戏元素,整体效果非常棒,在当地也小有名气了。

练习口语,尤其是初期,不能太在乎面子。比如说,不要太在乎发音。如果有机会和老外交流,就会发现这个世界上千奇百怪的口音多了去了,但这些口音并不会掩盖他们优秀的英语水平。也不要太在乎说错,我们即便说中文都会有嘴瓢的时候,为什么还在乎说外语出个错呢?出错了,才有机会纠错,才有机会不断进步。

把自己当小学生

很多朋友说英语的时候总是会磕吧,原因是嘴跟不上脑子。但是为什么嘴会跟不上脑子了,要把这个问题说明白并不容易。

我曾经听到过这么一个说法:“中国人口语差,很大程度上是因为他们的中文太好了。”

我感觉特别有道理,让我分享下我的理解。很多朋友初期说英语的时候,会先在脑子里面想好怎么用汉语回答,然后再翻译成英语,最后说出来。但是汉语是我们的母语,而且汉语和英语又归属于差别很大的两大语系。所以当我们在脑子里面试图把想好的华丽文字翻译成英语的时候,瞬间就卡壳了,然后就“呃呃呃……”

所以我的方法是,强迫自己成为小学生。每次要需要用英语表达的时候,都用最简单的主谓宾句式来构思,什么成语、谚语、歇后语,全部都不要。这样子在转换成英语说出来的时候就会容易得多,不至于会卡壳。随着用英语交流的来来回回变得顺畅后,你的自信心也会得到很大的提升,会给自己更多的动力继续坚持下去。

把自己当外国人

在和英语非母语的朋友用英语交流的时候,比方说两个中国人在英语角说英语,或者最近我和波兰同事开会,都可能遇到一种情况。就是你说了一个单词或者一个词组,对方听不懂。如果没有足够的练习,接下来一定就是一阵尴尬的沉默 😄。

所以,当我成为了小学生之后,我就开始尝试让自己成为一个外国小学生。我开始慢慢拒绝所有英汉词典,改用英英词典。这样子久而久之,当别人听不懂我的英语表达时,我可以用英语用另外一种方式解释给他听。我个人感觉,这是个非常有用的英语沟通技能。另外,我也很久没有对着单词表背过单词了,我觉得翻英英词典而接触到的新单词非常容易被记住。以此同时,Lexico 也成为了我最爱访问的站点之一。

假装自己身处外语环境

经常听到一些人想当然的说:学英语就是要有英语环境,如果能在国外住上几个星期英语绝对能突飞猛进。这话对也不对,学英语的确需要英语环境。但并不是呆在国外就能行。呆在国外英语也没啥进步的人,我身边见得多了。在都是华人的团队里面,上班说汉语,下班和家人也是说汉语。也就出门购物和吃饭,会偶尔说上几句英语。即便几年过去了,英语水平几乎没啥明显进步。

我个人先从看剧只开英文字幕开始,即便初期很多都听不懂,但是英文字幕也能提升阅读速度。我虽然也很喜欢看电影,但是我看的电视剧远多于电影,因为电视剧的对白更贴近日常,更能让我自己沉浸的更真实。如果因为字幕难度太高,依然跟不上的话,可以从英文动画片开始。因为大多数的英文动画片,语速比较慢,而且相对用词比较简单。为什么我用的是大多数,因为有例外,比方说 Rick and Morty,那语速和词汇量简直是噩梦。很长一段时间之后,在看片的时候,我会刷手机。如果发现自己跟不上了,就倒回去看。我承认这样子看片很费时间,但是这样子才能慢慢让自己听英语可以和听汉语一样。比方说,我现在就在边写日志边看少年谢尔顿。因为我们在听别人用汉语唠嗑的时候,并不需要多集中精神。多亏了这个练习,我现在可以在午饭点边吃盒饭边和其他时区的同事开会了。除此之外,我在做其他一些不需要集中精神的事情,例如:通勤、做饭、刷网页等等,也会听英语播客、新闻、电视剧对白等等。顺便吐槽一下,我一直觉得听新闻非常难。因为新闻很短,而且大多数情况下上一条和下一条完全没有上下文关系。

说说我对听力变化和对英语敏感度的几个转折点:

  • 某一天,突然发现自己看片的时候,不会下意识的一直盯着下排字幕了。
  • 某一天,突然发现自己即便偶尔刷下手机也能够跟上电视剧的剧情了。
  • 某一天,突然发现自己可以捕捉到字幕里面的错误了。
  • 某一天,突然发现自己不配字幕也能大致明白剧情了。
  • 某一天,有个人突然和我说句英语,我也能 get 到他在说什么了。
  • 某一天,突然发现自己不配字幕完全不影响观影一些简单内容了。

以上的变化,是通过长期练习从量变到质变的过程。总而言之,需要有足够的耐性去练习。

当然不得不承认,身在一个真实的英语环境,如果能善用这个环境的话,你的英语提升将会变得非常快。至少,我在这一年多英语的提升速度远胜于之前,当然这也要感谢之前自己不懈练习打下的基础。

效果验证

在杭州工作的还是很愉快的,但是职业生涯永远都不可能尽善尽美的,所以我刚喜欢上练习英语后,我也尝试面试过几家海外公司,每次都意料之中的以沟通存在障碍而告终。直到多年后,随着我孩子的降生,我也希望真正让生活来点改变。我面试了四个公司,经历过印度人、爱尔兰人、美国人、意大利人、日本人、新加坡人、中国人的多轮英语面试,最终都拿到了 Offer。这个结果,给我自己坚持练习英语打了一剂强心针。

当然,要让自己的英语练习有成效,前提是,切忌自欺欺人。不要把学习英语作为你沉迷于刷剧、刷电影和刷动画片的借口。真正把他们用在英语学习上才是关键。

抓住一切练习英语的机会

进入现在的公司之后,我越发发现更多自己在英语上的短板,譬如说:英语写作。由于无法忍受自己写的稀烂的英语邮件,Grammarly 也成为了我最爱访问的站点之一。我重开博客一部分原因也是因为这个,还将其设置为了双语博客。不知道大家有没有发现,我的每篇文章都是有中英两个版本的,可以通过菜单和侧边栏选择自己想看的语言版本。我希望通过书写日志能进一步练习自己的英语写作。即便这篇日志,其实并不适合给英语母语者看,旨在不浪费任何一个练习的机会,我还是写了这篇日志的英文版 🤣。

就先分享到这吧。在这希望大家能够坚持下去。学习英语,练习是不可避免的。而且练习本来就不是一个轻松的过程,不要指望一蹴而就。动不动就放弃的,其实还是一开始就没整明白。如果此文能让任何一个人有一点点收获,我也就满足了。如果此文能帮助任何一个人入门英语练习,我就高兴坏了。

Hammerspoon : macOS 界的瑞士军刀

相信很多朋友在自己的电脑里面都有特殊化定制。这可能也是为什么很多人都非常不喜欢配置新电脑或者重装系统。

譬如我自己,有一波应用用来提升我的电脑使用体验,如下是不完全统计:

应用用途
SizeUp可以通过快捷键将窗口吸附在屏幕的上下左右以及死角。对于大屏幕非常有用,左侧文档,右侧代码
Pap.er可以自动定时将美丽的风景画设置成我的壁纸
Stretchly定时遮挡屏幕以强制休息
Lexico.com牛津英英词典官方网站
Cheatsheet用来查看当前应用的所有快捷键
New Terminal Here在当前目录打开终端
我的常用工具列表

这些工具在提升我的用机舒适度的同时,也在我造成了一些困扰,尤其重装系统和换电脑的时候。有时候是因为软件开始收费了,我需要寻找一个免费的替代品;有时候是因为停止维护很久不再支持最新系统了,我需要寻找一个新的替代品。直到有段时间,Mac 似乎都和我有仇一般,半年更换了三台电脑。忍无可忍之下,我开始寻找一个能够替代这些小工具的方法,终于被我找到了一个 MacOS 上的瑞士军刀: Hammerspoon

类似的工具其实也不少,但是 Hammerspoon 在定制化能力和易用性上找到了一个平衡点。用户只需要稍微了解一下 Lua 基础就能很快上手利用 Hammerspoon 通俗易懂的 API 和完整的 Spoon 库实现一些定制化功能。我在毫无 Lua 基础的情况下,很快通过阅读文档完成了几个功能的定制。

我们可以用官方提供的 Spoon 来定制,以窗口控制为例

-- 我们可以通过引入 WinWin 这个 Spoon 来轻松实现
hs.loadSpoon("WinWin")

-- 判断 WinWin 是否正常载入,并根据窗口控制效果配置热键
if spoon.WinWin then
  -- 文本提示能完美支持符号字符,这对后面设置热键列表非常有用,后面会说
  -- Side
  hs.hotkey.bind({"cmd", "alt", "ctrl"}, "left", "Window ⬅", function() spoon.WinWin:moveAndResize("halfleft") end) 
  hs.hotkey.bind({"cmd", "alt", "ctrl"}, "right", "Window ➡", function() spoon.WinWin:moveAndResize("halfright") end) 
  hs.hotkey.bind({"cmd", "alt", "ctrl"}, "up", "Window ⬆", function() spoon.WinWin:moveAndResize("halfup") end) 
  hs.hotkey.bind({"cmd", "alt", "ctrl"}, "down", "Window ⬇", function() spoon.WinWin:moveAndResize("halfdown") end) 
  -- Corner
  hs.hotkey.bind({"shift", "alt", "ctrl"}, "left", "Window ↖", function() spoon.WinWin:moveAndResize("cornerNW") end) 
  hs.hotkey.bind({"shift", "alt", "ctrl"}, "right", "Window ↘", function() spoon.WinWin:moveAndResize("cornerSE") end) 
  hs.hotkey.bind({"shift", "alt", "ctrl"}, "up", "Window ↗", function() spoon.WinWin:moveAndResize("cornerNE") end) 
  hs.hotkey.bind({"shift", "alt", "ctrl"}, "down", "Window ↙", function() spoon.WinWin:moveAndResize("cornerSW") end) 
  -- Stretch
  hs.hotkey.bind({"cmd", "alt", "ctrl"}, "C", "Window Center", function() spoon.WinWin:moveAndResize("center") end) 
  hs.hotkey.bind({"cmd", "alt", "ctrl"}, "M", "Window ↕↔", function() spoon.WinWin:moveAndResize("maximize") end) 
  -- Screen
  hs.hotkey.bind({"alt", "ctrl"}, "right", "Window ➡ 🖥", function() spoon.WinWin:moveToScreen("next") end) 
  -- Other
  hs.hotkey.bind({"cmd", "alt", "ctrl"}, "/", "Window Undo", function() spoon.WinWin:undo() end) 
end
Code language: Lua (lua)

是不是很简单?只需要定义几个热键,选择一下需要的控制效果就 OK 了。

我们也可以利用 Hammerspoon 的 API 自定义 Spoon ,以 BreakTime 为例

为了实现这个,我们需要定时使用一个页面或者图片遮挡整个屏幕以提示电脑前的家伙应该短暂休息一下了。

首先,我们可以轻松通过 hs.timer 开启一个定时器:

obj.Timer = hs.timer.new(60, refresh)
obj.Timer:start()
Code language: Lua (lua)

然后,我们可以通过 hs.webview 创建一个遮挡这个屏幕的网页,页面中展示一个半透明的图片以达到透明效果

function makeBrowserOfBreakTime ()
  local screen = require"hs.screen"
  local webview = require"hs.webview"

  local mainScreenFrame = screen:primaryScreen():frame()
  browserFrame = {
     x = mainScreenFrame.x,
     y = mainScreenFrame.y,
     h = mainScreenFrame.h,
     w = mainScreenFrame.w
  }

  local options = {
      developerExtrasEnabled = true,
  }

  -- local browser = webview.new(browserFrame, options):windowStyle(1+2+4+8)
  local browser = webview.new(browserFrame, options):windowStyle(1+2+128)
    :closeOnEscape(true)
    :deleteOnClose(true)
    :bringToFront(true)
    :allowTextEntry(true)
    :transparent(true)

  return browser
end
Code language: Lua (lua)

接下来,我们就可以通过定时器来创建 browser:show() 和销毁 browser:delete() 页面来实现遮挡效果。

function refresh()

  obj.curTime = obj.curTime + 1
  if obj.curTime > obj.microbreakInterval then
  
    obj.curMicrobreakCount = obj.curMicrobreakCount + 1
    if obj.curMicrobreakCount > obj.microbreakCount then
      hs.alert.show(obj.breakTime .. " minute break starts")

      local browser = makeBrowserOfBreakTime();
      browser:url("file://" .. hs.spoons.scriptPath() .. "BreakTime.html?time=" .. (obj.breakTime * 60 - 1)):show()
      hs.timer.doAfter(obj.breakTime * 60, function()
        if browser ~= nil then 
          browser:delete(); 
        end 
      end)

      obj.curMicrobreakCount = 0
    else
      hs.alert.show(obj.microbreakTime .. " second microbreak starts")

      local browser = makeBrowserOfBreakTime();
      browser:url("file://" .. hs.spoons.scriptPath() .. "BreakTime.html?time=" .. (obj.microbreakTime - 1)):show()
      hs.timer.doAfter(obj.microbreakTime, function() 
        if browser ~= nil then 
          browser:delete(); 
        end 
      end)
    end

    obj.curTime = obj.curTime - obj.microbreakInterval

  end
end
Code language: Lua (lua)

最后,我们就可以像前面调用 WinWin 一样载入 BreakTime 这个自定义 spoon 并启动就可以了。

我们还可以定义任务栏菜单和提升一下用户体验

以 BreakTime 为例,我们可以将下一个休息时间显示出来

BreakTime 菜单提示
BreakTime 菜单提示

不光如此,我们还可以把所有在 Hammerspoon 定义的热键都展示出来。

所有热键的菜单提示
所有热键的菜单提示

由于他对符号字符支持的很完美,所以你在菜单里面可以设置很多有意思的提示,是不是很赞?😄

obj.menubar:setTitle("⌨️")
obj.menubar:setTooltip("Hot Key Info")

local hotkeys = hs.hotkey.getHotkeys()
local menuItem = {}
  
for key, value in pairs(hotkeys) do  
  local item = { title = value["msg"] }
  table.insert(menuItem, item)
end 

obj.menubar:setMenu(menuItem)Code language: Lua (lua)

除以上这些,Hammerspoon 还有很多东西值得探索。各位如果有兴趣的话,可以用我的定制随便试试。我也在不断摸索完善我自己的定制。

其实不光这些小工具,每次重新安装我的常用 App,例如 VS Code、Plex、Sublime Text、VIM 等等,也是个很痛苦的事情。我现在通过维护一套 homebrew 列表来快速完成大多数的 App 安装。😂