2024年7月28日星期日

做给 GitHub Actions 开发者用的 Actions(Part 2)

在开发 GitHub Actions 时,有时候会遇到这样的问题:如果这个 Action 接受来自用户的 GitHub token,那该如何以这个 token 背后的身份完成所需的 git 操作?

我在上一篇文章里介绍了我做的 config-git-with-token-action,专门用来解决这个问题的,但这个 Action 在配置 git 的时候又是怎么获取对应的用户名和邮箱的呢?这是通过另外一个叫做 token-who-am-i-action 来解决的。

这个 Action 可以接受用户生成的 PAT (Personal Access Token),也可以接受 GitHub App 的 token。(默认的 GitHub Actions token 当然是接受的。)它会使用 Action 调用 GitHub API 查询关于 token 自己身份相关的信息,因为类似于 Linux 的 whoami 命令所以我把它叫做 token-who-am-i-action


这个 Action 能返回的信息当中,首先要看 typeUser 还是 Bot

  • 如果是 User 的话,它所返回的其他信息包括 nameemail。这两项都可能是 undefined,因为用户可以选择隐藏自己的名称和邮箱。
  • 如果是 Bot 的话,它必须返回 nameemailappSlug,全部都是字符串,不可能是 undefined。每一个 GitHub App 必须有一个用于显示的名字,然后由此生成对外公开的地址(格式为 https://github.com/apps/${appSlug})。GitHub App 其实并不存在邮箱,但使用特定格式(${id}+${login}@users.noreply.github.com)生成的邮箱会被正确识别并显示正确的头像,例如说 GitHub Actions 默认 token 的身份使用的邮箱是 41898282+github-actions[bot]@users.noreply.github.com

除此之外,无论是哪种类型的身份,这个 Action 还能返回 loginidglobalIdlogin 对于 User 来说,就是他的个人页面地址(https://github.com/${login})中的路径,有些文档也把这个叫做 username;对于 Bot 来说,这可以由 appSlug 通过特定格式(${appSlug}[bot])生成,例如 GitHub Actions 的就是 github-actions[bot]

至于 idglobalId 分别用于 REST API 和 GraphQL。这是 GitHub 数据存储很有意思的一个地方。对于每一种 REST API 的类型(如 user),它背后都是一张独立的关系型数据表,都有一个这种类型内部唯一的 id,但这个 id 可以跟其他类型冲突。在 GraphQL 里面,因为 id 可以用来查询任何类型,所以引入了 globalId 的概念,保证即使跨类型依然唯一。GraphQL 的某些 query/mutation 的 id 默认就是 globalId;但某些必须加上 X-GitHub-Next-Global-ID: 1 的 header 进行请求才是 globalId,否则就是跨类型不唯一的 id


那我们如何在编写自己的 Action 时调用 token-who-am-i-action 呢?如果你在编写的是 composite action,可以这样写:

runs:
  using: 'composite'
  steps:
    - uses: CatChen/token-who-am-i-action@v1
      id: token-who-am-i
      with:
        github-token: ${{ inputs.github-token }}

    - shell: bash
      env:
        LOGIN: ${{ steps.token-who-am-i.outputs.login }}
        GLOBAL_ID: ${{ steps.token-who-am-i.outputs.global-id }}
        ID: ${{ steps.token-who-am-i.outputs.id }}
        NAME: ${{ steps.token-who-am-i.outputs.name }}
        EMAIL: ${{ steps.token-who-am-i.outputs.email }}
        TYPE: ${{ steps.token-who-am-i.outputs.type }}
        APP_SLUG: ${{ steps.token-who-am-i.outputs.app-slug }}
      run: |
        echo "Login is $LOGIN"
        echo "Global id is $GLOBAL_ID"
        echo "Id is $ID"
        echo "Name is $NAME"
        echo "Email is $EMAIL"
        echo "Type is $TYPE"
        echo "App slug is $APP_SLUG"

如果你编写的是 JavaScript action 可以先从 NPM 安装同名的 token-who-am-i-action 包,然后再进行调用:

import { tokenWhoAmI } from 'token-who-am-i-action';

const me = await tokenWhoAmI(githubToken);

const {
  login,
  globalId,
  type,
} = me;

if (me.type === 'User') {
  const {
    id,
    name,
    email,
  } = me;
} else if (me.type === 'Bot') {
  const {
    appSlug,
    id,
    name,
    email,
  } = me;
}

希望这个 Action 对各位 GitHub Actions 开发者有用。非 Actions 开发者也可以直接在 Workflow 里面使用这个 Action,如果你的 Workflow 使用非默认的 GitHub Actions(机器人)身份进行 git 操作的话。大家在使用过程中遇到什么问题,或者是希望增加什么新功能,欢迎到项目的 GitHub 开 issue。

2024年6月30日星期日

做给 GitHub Actions 开发者用的 Actions(Part 1)

在开发 GitHub Actions 时,有时候会遇到这样的问题:如果这个 Action 接受来自用户的 GitHub token,那该如何以这个 token 背后的身份完成所需的 git 操作?

使用 token 操作 GitHub API 是很容易的,通过 @actions/github(或 @octokit/core)创建一个 Octokit 实例时把 token 传进去就可以了,之后通过这个实例进行的所有 API 调用(包括 REST 和 GraphQL)都会以这个 token 的身份进行。但命令行的 git 操作怎么办呢?如何让 git commit 的作者变成 token 背后的身份?如何让 git push 以 token 背后的身份进行提交?(这两者并不一定要用同一个身份。)我做了 config-git-with-token-action 就是用来解决这个问题的。

这个 Action 会对 ghgit 进行配置,让它们的身份信息变成 GitHub token 背后的身份信息。gh 的配置相对简单一些,把 GH_TOKEN 这一环境变量配置好就行了,然后执行 go auth status 就能打印出 gh 认为自己在使用的身份信息。对 git 进行配置稍微麻烦一些,gh auth setup-git 只能保证 git 在跟 GitHub 交互时从 gh 获得身份信息,但并不指明具体是哪一个身份。为了保证 git commit 使用正确的身份,需要通过 git config 来设置正确的用户名和邮箱。此外,为了保证 git push 使用正确的身份,需要通过 git remote set-url origin 来更新上游地址,在 https://github.com/… 的地址中注入用户名和 token,让它变成 https://username:token@github.com/…

这个 Action 假设用户在执行它之前已经执行过了 @actions/checkout,所以它不会尝试自行建立项目目录。大家最好使用同一个 token 进行 @actions/checkout,这样项目目录从一开始就是以 token 背后的身份创建的。以下是一个完整的用例:

runs:
  using: ‘composite’
  steps:
    - uses: actions/checkout@v4

    - uses: CatChen/config-git-with-token-action@v1
      with:
        github-token: ${{ inputs.github-token }}

    - shell: bash
      run: |
        echo “Set up git user name: $(git config —get user.name)”
        echo “Set up git user email: $(git config —get user.email)”
        echo “Set up git remote origin with login and token: $(git remote get-url origin)”

    - shell: bash
      run: |
        touch test_file
        git commit test_file -m ‘Created test file’
        git push

做为 GitHub Actions 开发者,如果你利用 JavaScript 而非 bash 进行开发,那上述 composite action 的用例并不适用,我们需要一个针对 JavaScript action 的用例。我自己有同样的需求,所以 config-git-with-token-action 同时还是一个 NPM 包,可以在 JavaScript 中进行调用获得同样的功能。(这个包具备完整的 TypeScript 类型信息。)安装好之后,通过 JavaScript 调用的用例如下:

import { configGitWithToken } from ‘config-git-with-token-action’;

await configGitWithToken(githubToken);

希望这个 Action 对各位 GitHub Actions 开发者有用。非 Actions 开发者也可以直接在 Workflow 里面使用这个 Action,如果你的 Workflow 使用非默认的 GitHub Actions(机器人)身份进行 git 操作的话。大家在使用过程中遇到什么问题,或者是希望增加什么新功能,欢迎到项目的 GitHub 开 issue。

至于这个 Action 是如何获取到 token 背后的身份信息的,那是这个系列的下一篇文章要介绍的下一个 Action 负责的。

2024年3月1日星期五

如何扩大工作的 scope?

在美国工作的程序员都会遇到一个问题:想要晋升但经理说自己工作的 scope 不够大,又或者是影响力不够大。那要如何才能扩大自己工作的 scope 呢?

职业前期直系经理会负责为你找 scope,一步一步地给你更大的 scope,到了后面经理就会说「自己找 scope,这是晋升到下一个级别的要求,我给你 scope 就满足不了这个要求了。」既然不能依赖别人给你 scope,那要去哪里找扩大 scope 的机会呢?

这是很多人感觉到困扰的问题。假设自己在做的 scope 大小算作 1,把上下左右的外延做了也就是 1.1、1.2、1.3……的增长,这扩张速度太慢了。找一个跟自己已有 scope 相似但需要花同样功夫的 scope,那可以从 1 变成 2,但工作量跟着翻倍,拼命卷可以升一级,但 scope 大小从 2 到 3 是不可能的。况且职级线性上升时需要的 scope 是指数上升的,卷到三倍的工作量也没用,公司期望 scope 按照 1、2、4、8……的速度来增长。


在这篇文章里面,我们的关注点是如何找到更大的 scope,不是有了更大的 scope 后怎么做出来。怎么做出来当然是个难题,但很多人被卡在了找不到 scope,所以必须先解决这个问题。找不到 scope,不意味着找到了就有能力做出来,但找不到的时候人就会觉得自己有能力无处施展,这会导致很强的挫败感。

如何能找到更大的 scope?关键是要走出去跟更多人接触。技术再厉害的人,把自己关在小黑屋里面对着代码库发呆,是几乎不可能找到更大的 scope 的。你只能看到你已经知道的问题和机会在哪里,最多再看到一点外延,但指数级扩大的问题和机会很难闭关冥想出来。

我们可以在白纸上画三个从小到大的同心圆,分别代表:我能控制的事情、我能影响的事情、我能感知的事情。如果只考虑工作上的事情,这三个集合应该是两两子集的关系,「我能控制的事情」是「我能影响的事情」的子集,「我能影响的事情」是「我能感知的事情」的子集。

找不到更大的 scope,问题往往来自于「我能控制的事情」扩张到逼近「我能感知的事情」的边界了,然后就找不到空间继续扩张了。想要对着「我能控制的事情」大力出奇迹是很难有效果的,把工作量翻倍后 scope 依然上不去。真正能扩大 scope 的,是先把「我能感知的事情」扩张出去,有空间了再把「我能影响的事情」扩张出去,最后才能把「我能控制的事情」扩张出去。

如何能够把「我能感知的事情」扩张出去?我们必须走出去,接触更宽广的世界,接触更多各式各样的人。在一家大厂内部,这往往就是走出去跟别人聊天。我们对自己每天在做的项目、在解决的问题有太深的了解了,我们需要了解别人尝试解决什么问题、有什么问题解决不了,更大的 scope 就埋藏在那里。


如何走出去跟别人聊天?可以先从已经跟你有交集的人开始。肯定存在一些人,项目上跟你有合作,但有限的合作导致你们的交互很少,你不知道在合作之外他的主要工作是什么。你可以约他们喝咖啡,不要聊你已经知道的事情,问一下他在合作以外的主要工作是什么,以及他的工作有什么有意思的地方、有什么难题或者是不爽的地方。

关键是你要会聆听,他说的事情你要么理解了,要么通过更精准的问题追问。想象一下你跟他聊天后需要回来汇报跟他相关的所有信息,你自然会仔细听,甚至会做笔记,不会瞎扯一番大家爽了但谁也不记得聊了什么。这个对话应该像个自然的社交聊天,你不能如同审问对方一样获取信息。对方说的事情,你有共鸣的可以回应,你有不同观点的可以分享,这才是一个对话。

职级比较低的人,对于找职级比较高的人聊天可能存在心理上的障碍,觉得不应该浪费对方宝贵时间,错误假设自己懂的对方都懂了,所以对方不会从对话中获得任何价值。其实职级比你高的人也是人,你跟他们聊天可以提供情绪价值,有时候还能帮他们理清思路。如果长远来看你们会有更多合作,他们肯定乐意花时间跟你建立良好的信任关系,这包括花时间互相了解对方,尤其是对方的思维方式、思考问题的出发点。

刚刚开始跟几个人聊天时,你会获得大量碎片化的信息。聊的人多了之后,慢慢这些信息就会连接起来,呈现出公司内部更大的图谱。你需要在这个更大的图谱当中寻找当前运作得不够好的地方,例如说可靠性低或者是效率低的地方,然后想想你是否有想法和有能力去解决,这些都是你潜在的 scope。可能你没有信心直接玩起袖子开干,但你至少有素材跟你经理进行对话,说说你看到的机遇在哪里,让他就什么机遇更适合你提出他的看法。

2024年2月18日星期日

Vision Pro 使用体验(Part 2)

Vision Pro 的「截屏」功能非常符合 iOS 和 watchOS 用户的习惯,把仅有的两个物理按键同时按下去就可以了。跟用 Vision Pro 拍摄 3D 视频没有取景框一样,截屏时你没办法知道截屏边界在哪里。Vision Pro 给你一个相当宽的可视角度,但实际截屏时它会截取一个 16:9 的区域然后保存为 1920x1080 的图片。

考虑到 Vision Pro 两块屏幕加起来像素超过 4K 屏幕,截屏 1920x1080 的分辨率实在是有点低。你可以截屏分享给别人,让别人看看你佩戴 Vision Pro 的体验是怎样的,但千万不要指望别人能够看清楚你第一人称视觉能看清楚的细节,更别期望别人能看清截屏上的小字。经过 3D 到 2D 的投射之后,截屏上偏小的文字是很难看清楚的,需要很用力地看才能看明白。

此外,我不知道 Vision Pro 截屏时使用的是左眼还是右眼的视觉,这值得研究一下。


把 Vision Pro 变成 Mac 外置屏幕的功能还不错。就算是 16“ MacBook Pro 的屏幕也只有 16”,但用 Vision Pro 打开瞬间可以变成 75" 的大屏幕,而且依旧看不到像素。这个屏幕可以用来玩游戏,游戏的计算应该是在 Mac 上进行的,Vision Pro 只是投屏而已。对于游戏来说,有一个巨大的投屏可比 MacBook Pro 的屏幕爽多了!而且这个投屏还不受物理空间限制,即使你的房间没那么大,放下 MacBook Pro 后就没多少空间了,你依然可以在 Vision Pro 里面放置一个突破房间墙壁限制的大屏幕。

比较遗憾的是缺乏一台 Mac 多屏幕投屏的支持。Vision Pro 只能显示 Mac 的主屏幕,不能选择增加屏幕。如果在投屏到 Vision Pro 之前 Mac 就已经连接了外置屏幕,投屏后所有外置屏幕都会熄灭。Mac 自身的主屏幕也会熄灭,既然投屏了就没必要在 Vision Pro 外面显示一模一样的内容了。这对于注重隐私的人来说会很有用,例如说在咖啡店或在飞机上用 Mac 加 Vision Pro 投屏,别人就看不到你的屏幕了。

想把 Mac 投屏到 Vision Pro 时,可以在 Vision Pro 里面抬头调出头顶上的 Control Center 然后选择连接附近的 Mac。更加神奇的连接方式是,在 Vision Pro 里面以穿透方式看着一台 Mac 的屏幕,Vision Pro 自动能够识别出这是哪台 Mac 然后在 Mac 的屏幕上方放置一个投屏按钮,点击按钮就会开始投屏。


反过来,你在 Vision Pro 里面看到的画面也可以投屏到 Mac 或 iPad 上。这很适合用来做演示,把自己在 Vision Pro 里面看到的内容分享给身边的人看。操作起来跟 iPhone 投屏到 Mac 上一模一样,在 Vision Pro 的 Control Center 选择 Screen Mirroring 就可以了。

访客模式是使用投屏的另外一个常见原因。visionOS 不像 macOS 那样存在多用户登录,更像是 iOS 那样只允许单一用户,只能够绑定单一 iCloud 帐号。如果想要把 Vision Pro 借给别人使用,就要开启访客模式,之后可以限制访客能够打开的应用(他会看到能打开的应用中你的所有数据)。为了更好的指导访客使用你的 Vision Pro,或者是为了更好地监控他在你的 Vision Pro 里面干什么,你可以在开启访客模式的同时投屏,那你就能看到它看到的画面了。


前面说到 Mac 投屏突破房间墙壁限制,我可以解释一下 visionOS 的 2D 窗口是如何跟现实世界的 3D 物品重叠的:应用的 2D 窗口永远会优先于现实世界的物品。举个例子,我可以在我和应用窗口之间放一个小盒子,理论上这个小盒子应该会遮挡窗口的一部分,而且 visionOS 确实能扫描到这个小盒子的存在。然而 visionOS 并不会让这个小盒子穿透显示到我的 Vision Pro 屏幕上,应用窗口即使在小盒子背后也会被优先显示出来,导致小盒子被隐藏起来。唯一例外的是我的双手,把手举起来放在 Vision Pro 和窗口之间,手是会被显示出来的,我可以看清楚手和窗口的互动操作。

Vision Pro 使用拇指和食指触碰一下表示单击,这是大家都在官方视频中看到的,但其实还有另外一种点击方式。只要把窗口拉到自己面前,手指可以直接点击窗口上的按钮,手指穿越 3D 空间中的 2D 窗口会被视为点击,手指穿越窗口后上下左右移动会被视为拖拽。这种设计使得 visionOS 里面显示的所有 2D 窗口名义上都是 iOS 一样的「触摸屏」,习惯触摸屏的用户会发现这非常符合直觉。


这次就写到这里吧,接下来想到有新内容再更新。这个系列的《Vision Pro 使用体验》,我准备想到哪里就写到哪里。不想错过接下来的内容的话,敬请关注和订阅。这篇文章首发于我的 Patreon,大家可以到 Patreon 上付费支持我写作。

2024年2月9日星期五

Vision Pro 使用体验(Part 1)

在 Vision Pro 之前,我有 Oculus Rift 和 Quest 2,这两个都是纯 VR 设备(Quest 2 在接近障碍物时会触发黑白穿透视频)。Vision Pro 跟它们比,最大的优势是 AR 不会晕,我连续使用几个小时都没问题。之前 Quest 2 玩 Light Saber 一天最多玩 20 分钟,然后就开始感觉到头晕了。 虽然 Vision Pro 是有点重,但习惯后并不会觉得难受,当然脱下来之后脸上还是会有一圈的痕迹。我选择的是到 Apple Store 店里提货,顺便做 fit test,保证扫脸得到的尺寸合身。我觉得 fit test 还是挺重要的,扫脸归扫脸,但最终你需要找到一个几乎不漏光且重量均匀分布在脸上的尺寸。如果重量主要落在额头、脸颊或左右两侧,都会感觉到不舒服。

Apple Store 的店员有一套专门的「合身调试流程」。他们会先按照扫脸的结果给你对应的尺寸,然后问你哪里有漏光、哪里重量不均匀、哪里压得你不舒服,根据你的回答来找不同的尺寸给你试。当然,这暂时只能在美国体验到。在其它地方购买别人选好的尺寸,不太合身也只能将就了。


Vision Pro 的视力矫正镜片本质上跟眼镜是一样的,分为处方眼镜和老花眼镜两种。老花眼镜 $100 一对,只有固定的几个度数。处方眼镜就是要验光才能生产的眼镜,支持近视(据说能到 1100 度)、远视(据说能到 500 度)、散光等,$150 一对。之所以叫做处方眼镜,是因为在美国必须要有最近 6 个月的眼光处方才能配。镜片由蔡司生产,必须上传美国处方并由蔡司验证后才会进行生产和寄送。

镜片的使用体验很好,轻轻放在 Vision Pro 屏幕上面就会通过磁吸固定。跟我之前佩戴眼镜使用 Rift 和 Quest 2 对比,不需要配搭眼镜的体验好多了!不需要顾及眼镜如何塞进设备有限的空间里面,不需要小心穿戴设备,戴上不需要调整眼镜立即能用。购买时 Apple 会强烈建议你在镜片上刻字(只能使用大写字母进行拼写),保证你的镜片就算不慎跟别人的镜片放在一起了也能迅速区分开来。(左右两片镜片有「L」和「R」的标记进行区分。)

两个人共用一台 Vision Pro 的话,切换物理镜片很方便,唯一问题是镜片取下来后没有专门用来存放的盒子,只能放回原包装盒里。visionOS 能够知道镜片更换了,需要重新校准目光,否则 Vision Pro 没办法精确地根据你看着哪里来点亮哪里。这个过程就比较麻烦了,要连续三次地点亮六边形六个顶点。就算两个人都有 Vision Pro 的锁屏密码,不需要开启 Guest Mode,切换镜片依然麻烦。


Vision Pro 的操作系统叫做 visionOS。对于习惯了 iOS 和 tvOS 的用户来说,visionOS 会显得完成度不够高。这不会影响使用,但会在很多细节上体现出来:

  1. iMessage 尚不支持 Contact Key Verification。这在最新版 macOS 和 iOS 都已经支持的功能,在最新的 visionOS 1.0.2 竟然还不支持。(尚处于 beta 的 visionOS 1.1 我没安装。)如果你的 iCloud 帐号已经启用了这一功能,在 visionOS 激活 iMessage 时就会出错。你必须在另一个设备上关闭这一功能后,才能在 visionOS 上再次登录 iMessage。
  2. 在 iOS 上 FaceTime 和 iMessage 同时出现在系统设置里面,如果遇到上述 iMessage 激活问题你会想要在 visionOS 系统设置里面找 iMessage 重新激活,然后你会发现 visionOS 系统设置只有 FaceTime 没有 iMessage。visionOS 会在桌面 iMessage 图表旁边显示一个叹号,让你在打开它时重新登录。visionOS 把 iMessage 当作一个普通的应用把它的系统设置中藏起来了。
  3. 除了能够显示多语言以外,国际化设置基本上不存在。输入法只有英文和 Emoji,Siri 只有英语,日期和时间格式的选项是不存在的,公制单位还是英制单位的选项也是不存在的。(多语言的字典倒是存在的,所有 macOS 上有的字典 visionOS 都有。)
  4. Game Center 必须使用 iCloud 帐号,不能如同 iOS 一样注销后再登录另一个 Apple ID。

Vision Pro 天然支持 3D 视频,因为左右两眼是两个独立的屏幕,可以显示不同的内容。Disney+ 的应用在 Vision Pro 里面会专门显示一个 3D 影片的分类,很多 Marvel 电影和 Pixar 动画片都有 3D 的版本。我打开了《Avengers: End Game》看了个开头,发现 3D 电影的效果很好。前景的人物和物件显然是立体的,背景的投射会随着你头部移动而进行轻微的调节,使得背景看起来有深度。这是 Vision Pro 跟 3D 电视不一样的地方,后者并不会跟踪你的头部移动。

Vision Pro 支持播放 iPhone 15 Pro 和 iPhone 15 Pro Max 拍摄的 3D 视频,同时也能进行 3D 视频拍摄。用 iPhone 拍摄的话,视频必须用横屏模式拍摄,因为这样 iPhone 才能使用两个摄像头从左右两个角度进行拍摄。拍摄好 3D 视频后,在 Vision Pro 里面打开照片应用就能看到(假设 iCloud 已经设置好,图片视频已经自动同步),其中专门有一个 3D 视频的分类帮你把 3D 视频从所有视频中筛选出来。

在 Vision Pro 里面播放自己录制的 3D 视频,感觉就像在眼前打开了一个传送门,看到传送门另一端 3D 的景象。平面视频会让你觉得原本 3D 的世界已经被强行投射到了一个平面上,只是这个平面上的画面在动。但 3D 视频看起来就像一个传送门,能够看到立体的人物和物件在动。3D 视频的播放器还有一个「全屏模式」,或者说是「沉浸模式」,把视频播放器的边框虚化为云雾一般,然后嵌入到你身处的场景当中去。

同样是拍摄 3D 视频,用 Vision Pro 的话没有取景框,而且拍出来的视频长宽比是 1:1,也就是正方形的。没有取景框的体验有点奇怪,你没办法确定什么被拍进去了、什么被剪裁掉了。此外 Vision Pro 拍摄视频的稳定性没有 iPhone 好,人带着 Vision Pro 拍摄视频时难免会随着目光转移而轻微移动头部,拍出来的视频就会有对应的轻微抖动,观看时就更有可能觉得不适。iPhone 拍视频时已经智能对视频做了稳定化,即使有一点点手抖拍出来的视频依然是非常稳的。

在可以选择的情况下,我会建议使用 iPhone 拍摄 3D 视频。iPhone 的相机默认是没有开启 3D 视频拍摄的按钮的,第一次使用之前必须先去系统设置打开,然后在拍摄视频的界面就会出现一个 Vision Pro 外圈形状的图标,用以开启 3D 视频拍摄。具体怎么操作可以看 Apple 官方的帮助文档


这个系列的《Vision Pro 使用体验》,我准备想到哪里就写到哪里。这次先写这么多,接下来想到有新内容再更新。不想错过接下来的内容的话,敬请关注和订阅。这篇文章首发于我的 Patreon,大家可以到 Patreon 上付费支持我写作。