Latest Learning

Stuff I learned recently.

SSL 证书过期会导致网站无法访问,用 CertMonitor 可以监控证书有效期并邮件通知。

快速开始

  1. 注册账号:访问 certmonitor.xyz
  2. 添加域名:输入要监控的域名
  3. 配置通知:设置邮件接收地址

为什么需要

  • Let’s Encrypt 证书 90 天到期,容易忘记续期
  • 自动续期失败时能及时发现

市售的食用鸡蛋几乎都是未受精的,无法孵化出小鸡。

原理

  • 食用鸡蛋: 养殖场只养母鸡或严格隔离公鸡,母鸡无需交配就能正常产蛋,这些蛋只是卵细胞,没有胚胎
  • 受精蛋: 需要母鸡和公鸡交配后产下,在 37.5°C 和适当湿度下孵化约 21 天才能出壳

如何区分

  • 照蛋: 用强光照射,受精蛋能看到血丝或胚胎痕迹
  • 打开看: 受精蛋的蛋黄上有小白点(胚盘),但生鲜时很难分辨

超市买的鸡蛋放多久都孵不出小鸡,只会变质。

GitHub token 校验不要依赖固定长度,尤其不要写类似 ghs_[A-Za-z0-9]{36} 的正则。

GitHub 官方文档列出的 token 前缀包括:

  • ghp_: classic personal access token
  • github_pat_: fine-grained personal access token
  • gho_: OAuth access token
  • ghu_: GitHub App user access token
  • ghs_: GitHub App installation access token
  • ghr_: GitHub App refresh token

2026-04-27 起,GitHub App installation token 会逐步切到 stateless 格式,ghs_ token 会变成类似 ghs_APPID_JWT 的结构,长度约 520 字符且会变化。JWT 部分也不应该由客户端解析或校验语义。

如果业务只是想拦住明显错误的输入,可以只做轻量格式检查:

const githubTokenRe = /^(?:(?:ghp|gho|ghu|ghs|ghr)_|github_pat_)[A-Za-z0-9_.-]+$/;

这个校验只确认是 GitHub 已知 token 前缀,并允许 JWT 常见的 .-。真正有效性仍然应该交给 GitHub API 返回 401 Bad credentials 之类的结果判断。

更稳的原则:

  • token 当作 opaque string,不解析内部内容
  • 不限制固定长度
  • 存储字段至少能放下 520 字符以上
  • Actions 里优先使用内置 GITHUB_TOKEN,跨 repo 或特殊权限才使用 PAT 或 GitHub App

参考:

  1. GitHub Docs: GitHub’s token formats
  2. GitHub Changelog: Notice about upcoming new format for GitHub App installation tokens

Workflowy 早期的 inline editing 不是直接用 contenteditable,而是把一个透明的 textarea 精确覆盖在当前 hover 的文本上,点击后再切换可见层。

实现思路:

  1. 页面上正常渲染文本内容。
  2. 鼠标 hover 某一项时,把一个 opacity: 0textarea 移动到这段文本上方。
  3. 点击时 focus textarea,把 textarea 设为 opacity: 1,同时把底层文本设为 opacity: 0
  4. 输入时同步 textarea.value 到底层渲染节点。
  5. blur 后恢复底层文本显示,隐藏编辑框。

这样做的好处是输入、选区、键盘行为都交给原生 textarea 处理,同时显示层仍可以自己控制高亮、链接、tag 等富文本效果。

<!doctype html>
<meta charset="utf-8" />
<style>
  .item {
    position: relative;
    padding: 4px 8px;
    font: 16px/1.5 system-ui, sans-serif;
    white-space: pre-wrap;
  }

  #editor {
    position: absolute;
    z-index: 10;
    opacity: 0;
    resize: none;
    overflow: hidden;
    border: 0;
    padding: 4px 8px;
    margin: 0;
    font: 16px/1.5 system-ui, sans-serif;
    background: transparent;
    outline: none;
  }
</style>

<div class="item">Buy milk #todo</div>
<div class="item">Read https://example.com</div>
<textarea id="editor"></textarea>

<script>
  const editor = document.querySelector('#editor');
  let currentItem = null;

  document.querySelectorAll('.item').forEach((item) => {
    item.addEventListener('mouseenter', () => {
      if (document.activeElement === editor) return;
      moveEditorOver(item);
    });
  });

  function moveEditorOver(item) {
    currentItem = item;
    const rect = item.getBoundingClientRect();

    editor.value = item.textContent;
    editor.style.left = `${rect.left + window.scrollX}px`;
    editor.style.top = `${rect.top + window.scrollY}px`;
    editor.style.width = `${rect.width}px`;
    editor.style.height = `${rect.height}px`;
    editor.style.opacity = 0;
  }

  editor.addEventListener('mousedown', () => {
    if (!currentItem) return;
    currentItem.style.opacity = 0;
    editor.style.opacity = 1;
  });

  editor.addEventListener('input', () => {
    if (!currentItem) return;
    currentItem.textContent = editor.value;
  });

  editor.addEventListener('blur', () => {
    if (!currentItem) return;
    currentItem.style.opacity = 1;
    editor.style.opacity = 0;
  });
</script>

来源:How does workflowy implemented inline editing?

想让 AI 在不熟悉的领域里产出更好,先学那个领域的 glossary。

很多 vibecoding 前端看起来差,不一定是模型不会写,而是 prompt 里只有「菜单」「按钮」「页面」这种粗粒度词。换成更准确的 UI component 名称,模型会更容易命中它内部已经学过的 pattern。

例如不要只说:

做一个有菜单和按钮的设置页。

先补一点 domain knowledge,再说:

做一个 settings surface:
- 左侧用 sidebar navigation
- 顶部用 toolbar,包含 segmented control 和 search field
- 主区域用 form section、field group、select、switch、slider
- 危险操作放到 destructive action zone
- 保存状态用 inline validation 和 toast feedback

这不是堆术语,而是在给 LLM 更精确的索引词。对任何陌生领域都一样:先问 AI 或文档要一份 glossary,掌握关键概念和对象名,再开始让它做事。磨刀不误砍柴工。

可以先看这些网站补词汇:

  1. The Component Gallery:查 UI component 的标准名称、别名、定义和真实 design system 示例。
  2. Domain Maps:用 domain map 先建立陌生领域的概念坐标,再把关键术语喂给 LLM。

docker-compose.ymltty: truestdin_open: true 这两个配置项分别对应 docker run-t-i 参数。

tty: true

分配虚拟终端(Pseudo-TTY),让程序输出像在普通命令行中运行,保留颜色和交互式输出。

stdin_open: true

保持标准输入开启,即使没有 attach 到容器也能接收指令。

适用场景

Minecraft 服务器等需要交互式控制台的服务。

为什么 Minecraft 必须用它们

Minecraft 服务器有交互式控制台,可以:

  • 手动输入命令:/op yourname 给自己管理员权限,或 /stop 安全关闭服务器
  • 使用 docker attach 进入游戏控制台直接输入指令

如果没开这两个选项,docker attach 只能看输出,无法输入命令。

services:
  mc:
    image: itzg/minecraft-server
    tty: true
    stdin_open: true

总结:tty 负责输出,stdin_open 负责输入。

公司网络能审计 HTTPS 访问记录,并非”破解了加密”,而是利用了 TLS 握手阶段明文传输的 SNI(Server Name Indication)

原理

在 HTTPS(即 TLS)连接中,代理/防火墙无需解密流量,也无需建立 CONNECT 隧道,只需旁路观察 TLS 握手的第一个 ClientHello 报文。

TLS 握手流程(简化)

  1. TCP 三次握手
  2. 客户端发送 ClientHello(明文)
  3. 服务器响应 ServerHello
  4. 后续加密通信

ClientHello 中的 SNI

ClientHello未加密的明文数据,其中包含扩展字段。SNI 格式如下:

Extension: server_name
Hostname: example.com

代理/防火墙只需解析这个明文握手包,即可获取目标域名,无需:

  • 解密 TLS 流量
  • 建立了 CONNECT 隧道(传统 HTTP 代理模式)

SNI 是 TLS 扩展,用于解决单 IP 多域名场景。客户端必须在 ClientHello明文告诉服务器想访问的域名,否则服务器无法选择正确证书。

审计边界

  • ✅ 能看到:目标域名(SNI)、目标 IP、连接时间、流量大小、连接时长
  • ❌ 不能看到:URL 路径、查询参数、HTTP Header、页面内容、POST 数据

HTTPS(TLS)保护的只是 HTTP 请求内容(路径、Header、Body),但不会隐藏目标域名和 IP 等元数据。

防御方式

  • ECH(Encrypted Client Hello):TLS 1.3 扩展,使用 DNS 公钥加密 SNI
  • DoH/DoT:防止 DNS 查询泄露,但无法阻止 SNI 泄露
更彻底的审计:HTTPS 中间人解密(MITM)

企业可在终端安装根证书,代理动态生成假证书解密流量。但已不属于纯 CONNECT 透传。

根据 2026 年最新实测数据,简单有效的 email 地址防爬虫方法:

纯文本 email

方法阻止率
无保护0%
HTML Entities95%
HTML 注释98%
HTML SVG100%
CSS display: none100%
JS 拼接100%
JS 转换 (自定义函数)100%
JS AES 加密100%

可点击 mailto 链接

方法阻止率
无保护0%
HTML Entities100%
URL 编码96%
HTTP 重定向100%
HTML SVG100%
JS 拼接100%
JS 转换100%
JS AES 加密100%

推荐方案

最简单且有效:CSS display: none + 变化诱饵标签

<div class="email">ad@<span>email.</span>spencermortensen.<span>example.</span>com</div>
div.email > span:nth-child(2) { display: none; }

无 JS 依赖,完全无障碍可用。

推荐方案:JS 转换(自定义函数)

<span id="email">zibby example com</span>
const map = { zibby: 'hello', example: 'gmail', com: 'com' };
// 自定义转换逻辑

原理:HTML 源码只有乱码,需要在浏览器端用 JS 转换才能得到真实 email。

最强方案:JS AES 加密(需 HTTPS)

<span class="email">Kreuz2xa6xB8Fpjaa0lFgACNLO6n_Auu1CGjcG8z_Ec</span>

使用浏览器内置 SubtleCrypto 进行 AES-256 加密。

不推荐(破坏可用性)

  • 符号替换 (ag AT email DOT com) - 用户需手动还原
  • 图片 - 无法复制、屏幕阅读器无法读取
  • CSS content - 文本不可选中
  • CSS 文字方向反转 - 复制后是反的

关键发现:大多数爬虫很简单,即使最基础的混淆技术也能阻止 95% 以上的爬虫。建议组合使用多种技术。

来源: Email obfuscation: What works in 2026?

Docker Compose 默认会给卷名加上项目前缀(目录名),使用 name 属性可自定义卷名。

services:
  db:
    image: postgres
    volumes:
      - my_custom_volume:/var/lib/postgresql/data

volumes:
  my_custom_volume:
    name: "production_db_volume"

运行 docker compose up 后,docker volume ls 会显示 production_db_volume,而非 my-app_my_custom_volume

适用场景:

  • 多项目共享卷
  • 固定名字便于脚本编写和迁移

Docker Compose 中的 healthcheck 配置会覆盖 Dockerfile 中的 HEALTHCHECK 指令。

# docker-compose.yml
services:
  web:
    image: myapp
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

优先级:Dockerfile < Docker Compose。

为什么 Compose 优先?

遵循”运行时配置优于镜像构建配置”原则。Compose 覆盖 Dockerfile 的优势:

  • 灵活性:不同环境(开发/测试/生产)可配置不同的检查频率或超时时间
  • 依赖管理:配合 depends_oncondition: service_healthy 控制容器启动顺序
  • 集中管理:在 Compose 文件中一目了然,无需翻阅各镜像源码

AI 时代 LLM 经常需要读取 git diff,默认行为不要用 delta 修改。

Delta 安装时可能会自动添加配置:

[core]
    pager = delta

需要先移除这个配置,然后在 .gitconfig 中添加:

[alias]
    ds = -c pager.diff=delta diff
  • git diff 保持原生格式,便于 LLM 读取
  • git ds 查看带 delta 的漂亮 diff 效果

Bitwarden SSH Agent 默认会暴露所有可用密钥给 SSH 客户端,可能导致 Too many authentication failures 错误。通过 SSH Config 指定特定密钥解决。

# ~/.ssh/config

Host vps1
  HostName domain.com
  StrictHostKeyChecking no
  User root
  Port 26456
  IdentityFile ~/.ssh/bw_keys/vps-auth.pub
  IdentitiesOnly yes

Host vps2
  HostName 1.1.1.1
  StrictHostKeyChecking no
  User root
  Port 27644
  IdentityFile ~/.ssh/bw_keys/vps-auth.pub
  IdentitiesOnly yes

IdentitiesOnly yes 强制 SSH 只使用指定的密钥,跳过 Agent 中的其他密钥。

/usr/bin/env 是 Unix/Linux 系统中用于查找并运行程序的工具,主要用于 Shebang 中以提高脚本可移植性。

核心作用与用法

核心作用:提高脚本的可移植性

1. 硬编码路径(不推荐)

#!/usr/bin/python3 - 直接指定路径,跨系统兼容性差。

2. 使用 env(推荐)

#!/usr/bin/env python3 - 在 $PATH 中搜索,兼容性极佳。

工作流程

  1. 内核读取 Shebang,运行 env
  2. env 查看 $PATH 环境变量
  3. 按顺序搜索 python3
  4. 找到后启动该程序

对比表

特性直接路径使用 env
可移植性极佳
虚拟环境支持不支持完美支持
灵活性

进阶技巧:临时设置环境变量

# 临时设置语言
env LANG=en_US.UTF-8 check_system_status

# 查看所有环境变量
env

注意事项

env 不支持传递多个参数,如 #!/usr/bin/env python3 -u 在旧系统上可能失效。

使用 brew install 命令安装某些 cask 软件时,可以省略 cask 关键字,直接 brew install yaak 也能安装成功。

但在使用 brew bundle 命令时,必须在 Brewfile 中明确指定 cask 关键字:

# 正确写法
cask "yaak"

# 错误写法,会报错
brew "yaak"

原因:brew bundle 解析 Brewfile 时需要明确区分安装类型,而命令行 brew install 有自动检测能力。

将 Git 仓库的默认分支从 master 修改为 main。

# 重命名本地分支
git branch -m master main

# 获取远程更新
git fetch origin

# 设置本地 main 分支跟踪远程 main 分支
git branch -u origin/main main

# 设置远程仓库的默认分支
git remote set-head origin -a

Formula 用于从源码编译,Cask 用于分发预编译二进制。

Homebrew 团队希望统一 CLI 和 GUI 的安装体验,推动 brew install 作为唯一入口。因此 GoReleaser 官方推荐使用 Cask:

  • 安装更快(无编译过程)
  • 自动化程度更高
  • 符合 Homebrew 官方发展方向

何时用 Cask:Go CLI 工具官方通常提供预编译二进制。

何时用 Formula:上游只提供源码、需要编译定制、或依赖本地库的软件。

自定义 tap 可同时包含两者:

# Formula
brew install zhaochunqi/homebrew-tap/<tool>

# Cask
brew install --cask zhaochunqi/homebrew-tap/<tool>

参考:GoReleaser v2.10 Blog Post

通过 homebrew-tap,可以让自己的项目通过 brew install 直接安装。

安装

brew install zhaochunqi/tap/git-open

创建自己的 tap

  1. 创建 tap 仓库,命名为 homebrew-<tap-name>,例如 homebrew-tap
  2. 在仓库根目录创建 Formula/<formula-name>.rb 文件

⚠️ 注意: Homebrew 官方不提倡在 Formula 中分发预编译二进制,这被认为是不好的实践。对于 Go CLI 工具等提供预编译二进制的项目,推荐使用 Homebrew Cask 进行分发。参考 Go CLI 工具更适合通过 Homebrew Cask 分发预编译二进制

class GitOpen < Formula
  desc "Open your git repo in browser using one command"
  homepage "https://github.com/zhaochunqi/git-open"
  version "2.2.1"

  on_macos do
    on_arm do
      url "https://github.com/zhaochunqi/git-open/releases/download/v2.2.1/git-open_Darwin_arm64.tar.gz"
      sha256 "9b653ba97f5095e8764f43eeb9ab0e46d01f4bbd14eadd51760b636512812a8c"
    end
    on_intel do
      url "https://github.com/zhaochunqi/git-open/releases/download/v2.2.1/git-open_Darwin_x86_64.tar.gz"
      sha256 "ab24e6fe8a49b6f526a785a94a10b56a139430f795346d30f8a5a5db1387223d"
    end
  end

  on_linux do
    on_arm64 do
      url "https://github.com/zhaochunqi/git-open/releases/download/v2.2.1/git-open_Linux_arm64.tar.gz"
      sha256 "8776da29b63a21f0949cda1814e30cc2c926bb6dee4199a9f7f0684486671c70"
    end
    on_x86_64 do
      url "https://github.com/zhaochunqi/git-open/releases/download/v2.2.1/git-open_Linux_x86_64.tar.gz"
      sha256 "cf91815149c341d718ea7b26d5d671e261bb8a5701e2de2da803d9a5a6c278c4"
    end
  end

  def install
    bin.install "git-open"
  end

  test do
    system "#{bin}/git-open", "--version"
  end
end

发布流程

  1. 项目打标签并发布到 GitHub Releases,上传预编译的二进制文件
  2. 更新 Formula 文件中的版本号和 sha256
  3. 提交到 homebrew-tap 仓库

相关链接

zsh 绑定键位

查看绑定键位

查看 ctrl + r绑定的键位::

 bindkey '^r'
"^R" history-incremental-search-backward

绑定新的键位

比如绑定 fzf-ghq-widget方法:

zle -N fzf-ghq-widget

# 1. 绑定默认 (Emacs) 模式
bindkey '^g' fzf-ghq-widget

# 2. 绑定 Vi 插入模式 (防止使用 Vi 模式时失效)
bindkey -M viins '^g' fzf-ghq-widget

解析:

  1. zle (Zsh Line Editor): 这是 Zsh 的核心模块,专门负责处理你在终端里打字、移动光标、删除文字这些交互操作。
  2. -N (New): 告诉 Zsh:“我要创建一个新的 Widget”。
  3. fzf-ghq-widget: 这是你要注册的名字。通常我们会让 Widget 的名字 和 函数的某些名字 保持一致,这样省事。