2026-01-19 11:03:00
2025 年,对我来说不是被某一个高光时刻定义的一年,而是一段在持续构建中慢慢成型的过程。
白天,我仍然在研发世界里与需求和 Bug 周旋,用一次次迭代换取稳定与确定;而当屏幕合上,生活开始显影:我们去旅行,把时间交给路途;牵手、拥抱,在忙碌与疲惫之间确认彼此的存在。
技术在向前,生活也在向前,而这一年最重要的关键词,或许正是这些看似柔软、却足够支撑长期前行的瞬间:旅行、牵手、拥抱,以及永远的我们。
去年换了新的工作,到了 2025 年,这种变化开始真正显现出来。
事情不再只是“完成任务”,而是被当作一份长期经营的事业来对待。工作时间更可控,选择空间也更大,节奏上不再是持续高压的忙碌,而是一种忙中有闲、但心里有数的状态。
在工作之外,我依旧活跃在开源世界中,5 月,我在 opensource.org 发布了我的开源故事:Lu Fei: Open Source Changed My Life – From User to Maintainer,因为有了更多可支配的时间,也重新提起了 docsify 的维护工作,今年发布了两个 RC 版本,目标是在 2026 年正式发布 5.0。
回头看这一年,技术没有离开中心,但它不再是消耗型的存在,而是逐渐成为支撑自由度、事业感和长期价值的底层能力。
如果说 2025 年的技术关键词是稳定与自由,那生活这条线,更像是一段被时间慢慢推着向前的故事。
一个很有意思的 callback 是:
去年,我和一位关系很好的同事一起离职;
今年,她成了我的女朋友。
这是蓄谋已久,还是命运绕了一圈?现在回头看,答案其实已经不重要了。
这一年,我们总是在路上,所有看似的错位,最后都成了走向彼此的路线。
3 月,她的一句“期待 3.14 我们青岛见”,我没去成,我是在月底之前才回去的,见了见以前的同事,聊了聊近况,吃喝玩乐一圈;
4 月,我去宁波出差,她去了亚庇;
5 月,她去郑州看 KPL 比赛,我在金鸡湖边喝咖啡。
那段时间,好像总差一步,却始终没有走远。
5 月中旬,我去了北京找她。一个人逛了国博、人民大会堂,那是第一次意识到:有些地方,一个人去是“参观”,两个人去才是“记忆”。
6 月,杭州出差,我去了灵隐寺、西湖、天下第一财神庙,替她请了一个手串,还特地坐了 1314 路公交车;
7 月,我第一次不想剪头发,想试试不一样的自己,染了她帮我选的颜色——树莓红;
8 月,秋天的第一杯奶茶,是霸王茶姬,送给我天下第一好的朋友。后来回头看,那大概已经不是一杯奶茶那么简单了;

9 月,我在南京,她在杭州,又一次错过。南京博物院、古鸡鸣寺、夫子庙,一个人走完,灵隐寺能斩孽缘,鸡鸣寺能斩乱桃花,哦~
直到 9 月底,她换了新工作,我们终于在杭州重合了行程。
一起爬天下第一财神庙,这次索道刚好检修,她一路等我,我爬得半死;路上还有位姐姐助攻:现在怎么还有小年轻出来爬山约会的。她没有反驳~
一起在西溪湿地坐了摇橹船,我们面对面坐着,船行得很慢,橹声一下一下落在水面上。视线会不经意地对上,又很快移开,听她说了一些工作上的槽点。
那一刻我其实很想问点什么,但话到嘴边,又咽了回去。很多话还没说出口,但距离,已经不是原来的样子了。
10 月,国庆,我们各自回到生活里。
我没回家,有天晚上出门去看了烟花,发给了她;她在我生日那天送了礼物,买了花和蛋糕。
国庆后错峰出行,我又去找她了。可能是看完《盛夏芬德拉》,终于学会了“长嘴”,我问她:
“你觉得我们还算是普通朋友吗?”
答案如我所愿。

那之后,北京的故事正式展开:
古代建筑博物馆、天坛公园、红螺寺、雁栖湖,爬山、骑车;
她看着我累得不行,笑得特别开心;
还有北京野生动物园,下车后往后勾勾的小手~
热恋期的我们,几乎每天都要打三四个小时的视频。
11 月,一起去了北京欢乐谷过万圣节,拍了那张用来给家里官宣的照片;
8 号一起去鸟巢看了 KPL 年度总决赛;
某个深夜,她因为太想我,买了一束花送到了我公司。

那一刻,想念终于有了形状。
11 月底,搬了新家,一起看了《疯狂动物城》。
“Love you, partner”
她第二次给我写了手写信,上一次,还是我们一起离职的时候。

12 月,我出差长沙,给她写了明信片;
看到剧里女主叠小星星写日记,我给她叠了 520 颗;
她带我去看了芭蕾舞剧《海盗》,居然没有睡着,原来我也可以这么“优雅”;

圣诞节的 callback,去年圣诞节在上海的匆匆一面,到今年的相拥而眠。
回看这一年,我们走过很多城市,也在不同节奏里慢慢靠近。
关系并不是某一个瞬间被确认的,而是在一次次同行中自然发生。

至于之后的路,不急着写完,
我们继续走下去就好。
2025-08-04 15:47:30
为了提升访问速度、增强稳定性并规避部分官方源的不确定性,将常用的开源镜像同步到中国大陆可访问的镜像仓库是一种高效的解决方案。
本文介绍如何通过 GitHub Actions 自动化完成该同步流程,支持选择性构建与定制版本。
许多开源镜像托管在 Docker Hub 上,但由于网络、访问频率限制等问题,拉取速度不稳定,甚至存在连接失败的情况。
之前是使用 GitHub Actions 同步到 CODING 上,不过要 CODING 要停服了,所以改为同步到 CNB 了。
cnb.cool 也是由腾讯出品,基于 Docker 生态,对环境、缓存、插件进行抽象,通过声明式的语法,帮助开发者以更酷的方式构建软件。
支持代码托管、云原生构建和云原生开发等功能。
skopeo 工具将镜像从 Docker Hub 同步到 CNB;点击查看完整文件内容
前往 GitHub 查看:docker-proxy.yml
name: Mirror Docker Images to CNB
on:
workflow_dispatch:
inputs:
name:
description: 'Select image to mirror (or leave blank to mirror all)'
required: false
type: choice
options:
- ""
- vaultwarden
- bark-server
- elasticsearch
- mysql
- hyperf
- clickhouse
version:
description: 'Override tag version'
required: false
type: string
push:
paths:
- '.github/images.yml'
branches: [ 'main' ]
jobs:
mirror:
name: >-
Mirror ${{ github.event.inputs.name || 'All Images' }}${{ github.event.inputs.version && format(' (version: {0})', github.event.inputs.version) || '' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Mirror images
env:
INPUT_NAME: ${{ github.event.inputs.name }}
INPUT_VERSION: ${{ github.event.inputs.version }}
run: |
images=$(yq -o json '.images' ${{ github.workspace }}/.github/images.yml)
if [ -n "$INPUT_NAME" ]; then
matrix=$(echo "$images" | jq -c --arg name "$INPUT_NAME" '.[] | select(.name == $name)')
else
matrix=$(echo "$images" | jq -c '.[]')
fi
if [ -z "$matrix" ]; then
echo "No matching images found for name: $INPUT_NAME"
exit 1
fi
echo "$matrix" | while read -r item; do
image=$(echo "$item" | jq -r '.image')
name=$(echo "$item" | jq -r '.name')
default_tag=$(echo "$item" | jq -r '.tag')
tag=${INPUT_VERSION:-$default_tag}
echo "Mirroring $image:$tag to docker.cnb.cool/lufei/docker/$name:$tag"
skopeo copy --all docker://docker.io/${image}:${tag} \
docker://docker.cnb.cool/lufei/docker/${name}:${tag} \
--src-creds "${{ secrets.DOCKERHUB_USERNAME }}:${{ secrets.DOCKERHUB_TOKEN }}" \
--dest-creds "cnb:${{ secrets.CNB_DOCKER_TOKEN }}"
echo "::notice title=Image Published::https://docker.cnb.cool/lufei/docker/${name}:${tag}"
done
on:
workflow_dispatch:
inputs:
name: # 可选镜像名
version: # 可选覆盖 tag
push:
paths:
- '.github/images.yml'
branches: [ 'main' ]
支持两种触发方式:
.github/images.yml 文件变更时,自动同步全量更新。.github/images.yml:
images:
- image: "vaultwarden/server"
tag: "latest"
name: "vaultwarden"
- image: "finab/bark-server"
tag: "latest"
name: "bark-server"
...
skopeo copy --all docker://docker.io/${image}:${tag} \
docker://docker.cnb.cool/lufei/docker/${name}:${tag} \
--src-creds "${{ secrets.DOCKERHUB_USERNAME }}:${{ secrets.DOCKERHUB_TOKEN }}" \
--dest-creds "cnb:${{ secrets.CNB_DOCKER_TOKEN }}"
--all:确保同步多平台镜像(如 amd64 和 arm64);在 GitHub → Actions → “Mirror Docker Images to CNB” → 点击 “Run workflow”:


每次更新 .github/images.yml 文件并推送到 main 分支时,会自动同步所有的镜像。
同步完成后,用户可直接通过 CNB 公网地址拉取镜像:
docker pull docker.cnb.cool/lufei/docker/hyperf:8.3-alpine-v3.21-swoole
加上https://可以访问网页查看详情:

| 名称 | 用途说明 |
|---|---|
DOCKERHUB_USERNAME |
用于拉取源镜像 |
DOCKERHUB_TOKEN |
Docker Hub 登录 token |
CNB_DOCKER_TOKEN |
CNB 镜像仓库推送凭据 |
CNB 默认可以创建 npm、Composer 等的制品库,但 Docker 的制品默认就在仓库中,所以创建一个仓库即可。
通过 GitHub Actions + skopeo + CNB 服务,我们构建了一个可复用、自动化、支持多镜像同步的工具链,显著提升了镜像的可用性与部署效率。
2025-04-18 17:04:42
之前因为 Google Adsense 要求进行新加坡税务信息填写,需要上传税务证明,但是实在找不到个人在哪里申请,于是就暂停了 Google Adsense 服务。
不过在 2025 年 4 月 1 日起施行了新规,申请《中国税收居民身份证明》不再困难了!
下面就来说说申请步骤:




Google Asia Pacific Pte. Ltd.
中华人民共和国政府和新加坡共和国关于对所得避免双重征税和防止偷漏税的协定
独立个人劳务条款

点击下一步提交确认等待主管机关审核即可。
审核通过后没有通知,需要自己去登录网站去查看,如果有问题的话,可能会被拒绝,或者主管机关会给你打电话。
目前我已经通过了,1 号政策开放的时候提交了一次,前几天主管机关下级给我打电话说没有下发成功,让我撤销重新提交一次,这次提交后两三天就通过了。
2025-03-06 17:19:07
在开发过程中,我们经常使用 HMAC(散列消息认证码)对数据进行签名,以确保数据完整性和身份验证。
然而,不同编程语言在对签名数据进行编码时可能会有所不同,导致相同的 HMAC 计算在不同语言中产生不同的结果。
这篇文章也是因为我直接将 PHP 的签名算法扔给 ChatGPT 生成,并没有实际测试,导致客户反馈签名计算失败,测试后才发现的。
本文将以 Go 和 PHP 为例,探讨为什么直接对 HMAC 签名进行 Base64 编码与先转换为 16 进制字符串再编码的结果不同。
package main
import (
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"encoding/hex"
"fmt"
)
func main() {
data := "hello"
password := "123456"
h := hmac.New(sha1.New, []byte(password))
h.Write([]byte(data))
signatureBytes := h.Sum(nil)
// 直接对 HMAC 结果进行 Base64 编码
base64Signature := base64.StdEncoding.EncodeToString(signatureBytes)
fmt.Println(base64Signature) // 输出:NYSQUfYBHG0EZ6pU+r+Iw4CvPIQ=
// 先转换成 16 进制字符串,再进行 Base64 编码
hexString := hex.EncodeToString(signatureBytes)
base64OfHex := base64.StdEncoding.EncodeToString([]byte(hexString))
fmt.Println(base64OfHex) // 输出:MzU4NDkwNTFmNjAxMWM2ZDA0NjdhYTU0ZmFiZjg4YzM4MGFmM2M4NA==
}
<?php
$data = "hello";
$password = "123456";
// 直接对 HMAC 结果进行 Base64 编码
echo base64_encode(hash_hmac('sha1', $data, $password, true));
// 输出:NYSQUfYBHG0EZ6pU+r+Iw4CvPIQ=
echo "\n";
// 先转换成 16 进制字符串,再进行 Base64 编码
echo base64_encode(hash_hmac('sha1', $data, $password));
// 输出:MzU4NDkwNTFmNjAxMWM2ZDA0NjdhYTU0ZmFiZjg4YzM4MGFmM2M4NA==
?>
表面上看,Go 和 PHP 代码的逻辑是相同的,但它们的 Base64 结果却不同。
其根本原因在于编码前的输入数据不同。
hash_hmac(
string $algo,
string $data,
#[\SensitiveParameter] string $key,
bool $binary = false
): string
PHP 手册中也提到了:当 binary 设置为 true 输出原始二进制数据,设置为 false 输出小写 16 进制字符串。
signatureBytes 是 HMAC 计算出的二进制数据。hash_hmac('sha1', $data, $password, true) 也返回二进制数据。hash_hmac('sha1', $data, $password) 默认返回 16 进制字符串,每个字节被转换成 2 个字符。hex.EncodeToString(signatureBytes) 也会将二进制数据转换为 16 进制字符串。Base64 编码的主要作用是将二进制数据转换为文本格式,便于在 URL 或 JSON 等环境中传输。
它不会改变数据的内容,而是按照固定的方式将每 3 个字节转换为 4 个可打印字符。
因此,输入数据的不同会直接影响最终的编码结果。
如果希望跨语言 HMAC 计算保持一致,建议:
hash_hmac('sha1', $data, $password, true) 以获取二进制结果。base64.StdEncoding.EncodeToString(signatureBytes),避免中间转换为 16 进制字符串。希望这篇文章能帮助你理解 HMAC 签名在不同语言中的编码差异,并在开发中避免类似的问题!
2025-02-27 14:42:30
在 DevOps 和开发流程中,如何安全高效地管理机密数据(如密码、API 密钥和认证信息)是一个重要话题。
Bitwarden 是一款开源密码管理工具,帮助用户存储、管理并共享敏感信息。Bitwarden 推出了新产品Secrets Manager,专为 DevOps 团队和开发人员提供简化的机密管理方案。
对于使用 GitHub Actions 等 CI/CD 工具的团队来说,Secrets 是一种存储机密信息的常见方式。
但是,GitHub Actions 中的 Secrets 一旦保存,就无法查看或修改,这使得本地保存机密变得繁琐且易出错。
而在团队环境中,个人和公司电脑之间的同步问题更是增加了额外的复杂性。
Bitwarden Secrets Manager 解决了这一难题,提供了安全、高效的机密存储与管理方式。

与传统的 Bitwarden 密码管理器类似,Secrets Manager 也支持 self-hosting,但需要授权才能进行。
为了简化流程(白嫖),也可以使用 Bitwarden 提供的在线服务,享受如下免费额度:
这些免费额度对于大多数小型或中型团队来说已足够使用。
Bitwarden Secrets Manager 便于与 GitHub Actions 等 CI/CD 服务进行集成。
以下是一个简单的示例,展示了如何在 GitHub Actions 中获取并使用存储在 Bitwarden 中的机密。
首先,在 GitHub Actions 的工作流 YAML 文件中,添加获取机密的步骤:
- name: Get Secrets
uses: bitwarden/sm-action@v2
with:
access_token: ${{ secrets.BW_ACCESS_TOKEN }}
base_url: https://vault.bitwarden.com
secrets: |
fc3a93f4-2a16-445b-b0c4-aeaf0102f0ff > SECRET_NAME_1
bdbb16bc-0b9b-472e-99fa-af4101309076 > SECRET_NAME_2
在上面的示例中,fc3a93f4-2a16-445b-b0c4-aeaf0102f0ff 和 bdbb16bc-0b9b-472e-99fa-af4101309076 是存储在 Bitwarden Secrets Manager 中的机密 ID,而 SECRET_NAME_1 和 SECRET_NAME_2 是引用机密的名称,用于在后续步骤中进行使用。
接着,在后续步骤中,使用这些机密值:
- name: Use Secret
run: SQLCMD -S MYSQLSERVER -U "$SECRET_NAME_1" -P "$SECRET_NAME_2"
完整示例:
- name: Get Secrets
uses: bitwarden/sm-action@v2
with:
access_token: ${{ secrets.BW_ACCESS_TOKEN }}
secrets: |
fc3a93f4-2a16-445b-b0c4-aeaf0102f0ff > GITHUB_GPG_PRIVATE_KEY
bdbb16bc-0b9b-472e-99fa-af4101309076 > GITHUB_GPG_PRIVATE_KEY_PASSPHRASE
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@v6
with:
gpg_private_key: ${{ env.GITHUB_GPG_PRIVATE_KEY }}
passphrase: ${{ env.GITHUB_GPG_PRIVATE_KEY_PASSPHRASE }}
git_user_signingkey: true
git_commit_gpgsign: true
更多集成方式请参考 官方文档。
为了便于在本地查询和管理机密,Bitwarden 提供了强大的 Secrets Manager CLI 工具。可以通过它来创建、删除、编辑和列出机密。
从 GitHub Releases 下载适合操作系统的可执行文件,运行以下命令以查看帮助信息:
bws --help
使用 Secrets Manager CLI 时,需先配置访问令牌(Access Token),然后运行如下命令列出机密:
# 设置环境变量
export BWS_ACCESS_TOKEN=xxxxxx
bws secret list
# 或者从命令行传入
bws secret list --access-token xxxxxx
为了进一步提高效率,写了一个小工具,帮助在 macOS 上快速查询并复制 Secrets。

以下是完整的 PHP 脚本示例:
<?php
$tokenName = !empty($argv[1]) ? trim($argv[1]) : '';
$accessToken = getenv('BWS_ACCESS_TOKEN');
$iconPngUrl = 'icon.png';
$json = `bws secret list -t '{$accessToken}'`;
if (!$json) {
echo json_encode(['items' => [['title' => 'Error: Failed to fetch secrets', 'valid' => false]]]);
exit;
}
$list = json_decode($json, true);
$items = [];
foreach ($list as $item) {
if (!empty($tokenName) && stripos($item['key'], $tokenName) === false) {
continue;
}
$items[] = [
'arg' => $item['value'],
'title' => $item['key'],
'subtitle' => $item['note'],
'icon' => ['path' => $iconPngUrl],
'valid' => true,
];
}
if (empty($items)) {
$items[] = [
'title' => 'No secrets found',
'valid' => false
];
}
echo json_encode(['items' => $items]);
exit;
Bitwarden Secrets Manager 为 DevOps 团队提供了一种更加安全、便捷的方式来管理和集成机密信息。
无论是与 GitHub Actions 集成,还是使用 CLI 工具进行本地管理,Bitwarden 都提供了简洁而强大的功能,帮助提升工作效率并确保敏感数据的安全。
2025-02-25 14:24:31
在 MySQL 数据库中,UTF-8 及其变体是最常用的字符集。
不同的 UTF-8 编码可能对大小写敏感性产生影响,主要包括以下几种:
utf8:MySQL 早期的 UTF-8 实现,最多支持 3 字节,无法存储部分 Emoji 字符。utf8mb4:MySQL 5.5+ 版本推荐使用的 UTF-8 编码,最多支持 4 字节,能够完整存储所有 Unicode 字符。MySQL 字符集搭配不同的排序规则(Collation)可能会影响查询的大小写敏感性。
常见的排序规则包括:
utf8_general_ci / utf8mb4_general_ci:不区分大小写(Case Insensitive,ci 代表 Case Insensitive)。utf8_bin / utf8mb4_bin:区分大小写(Binary,bin 代表按二进制存储,严格区分大小写)。utf8_unicode_ci / utf8mb4_unicode_ci:更符合 Unicode 规范的排序方式,不区分大小写。默认情况下,utf8_general_ci 和 utf8mb4_general_ci 在搜索时是不区分大小写的。
当 MySQL 表的字符集设置为 utf8_general_ci 或 utf8mb4_general_ci 时,使用 LIKE 或 = 进行查询时,默认是不区分大小写的。
例如:
SELECT * FROM users WHERE username = 'admin';
如果数据库中存储了 Admin、ADMIN 等,查询会返回这些所有匹配项。
如果需要执行区分大小写的查询,则需要:
ALTER TABLE users MODIFY username VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;
这样查询就会严格区分 admin 和 Admin。
SELECT * FROM users WHERE BINARY username = 'admin';
这样 admin 只会匹配完全相同的字符串,而不会匹配 Admin、ADMIN 等。
whereRaw 进行原生查询在 ThinkPHP 框架中,默认的 where 方法不支持直接使用 BINARY 进行查询,但可以通过 whereRaw 方法执行 MySQL 原生查询。
$result = Db::table('users')
->whereRaw("BINARY username = ?", ['admin'])
->find();
$result = Db::table('users')
->whereRaw("BINARY username LIKE ?", ['%admin%'])
->select();
这种方法可以避免默认的大小写不敏感查询,让 MySQL 进行更严格的匹配。
utf8_general_ci 和 utf8mb4_general_ci 默认不区分大小写。BINARY 关键字。whereRaw 方法执行 MySQL 原生查询,确保大小写敏感匹配。这样,你就可以在 ThinkPHP 框架中更灵活地处理 MySQL 字符集大小写敏感的问题。