Search

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 的名字 和 函数的某些名字 保持一致,这样省事。

配置方法

在所有的 git 全局配置中,配置:

# Configure Git to ensure line endings in files you checkout are correct for macOS
git config --global core.autocrlf input

或者在对应的 ~/.gitconfig 中配置:

[core]
	autocrlf = input

但是 github 推荐在 windows 中将这个选项设置为 true,我查了下觉得在三端均采用 input 是非常合理的。

配置说明

配置值含义
true双向转换:
 • 输入(commit)时:CRLF → LF
 • 输出(checkout)时:LF → CRLF
input仅输入时转换:
 • 输入(commit)时:CRLF → LF
 • 输出(checkout)时:不做转换(保留 LF)
false完全不转换

参考链接:https://docs.github.com/en/get-started/git-basics/configuring-git-to-handle-line-endings?platform=linux

CR

“CR” 在电池型号中是标准命名:

  1. C = 锂 - 二氧化锰(Lithium-Manganese Dioxide)
  2. R 代表形状:圆形

后面的数字代表尺寸:

  1. 前两位(20):直径,单位是毫米(mm)→ 直径 20mm
  2. 后两位(32):厚度,单位是 0.1mm → 3.2mm 厚

结论

  1. CR2032 = 锂锰电池,圆形,20mm 直径,3.2mm 厚
  2. CR2025 = 锂锰电池,圆形,20mm 直径,2.5mm 厚

Unleash 是一个企业级的 Feature Toggle 管理平台,核心价值是解耦部署与发布

解决的问题

  1. 部署与发布分离:代码可以部署到生产环境但默认关闭,通过控制台按需开启
  2. 主干开发:避免长期分支的合并冲突,半成品代码可安全合并到主分支
  3. 灰度发布:支持渐进式流量释放(1% -> 10% -> 100%),降低发布风险
  4. A/B 测试:基于用户 ID 的分组分流,为产品决策提供数据支持
  5. 零停机修复:发现问题可秒级关闭开关,无需代码回滚

架构特点

Unleash 采用本地求值架构:

  • Server 只推送规则策略,不接触用户数据
  • SDK 在应用本地内存中判断开关状态
  • 即使 Server 宕机,应用仍可正常工作(缓存策略)
  • 用户隐私数据(GDPR)不离开服务器

核心策略

  • Standard:全量或关闭
  • Gradual Rollout:渐进式释放百分比
  • UserID:针对特定用户白名单
  • Flexible Rollout:A/B 分组测试

今天学到了可以在对应的 pr 中添加 .diff 直接查看 diff, 然后可以将链接直接丢给在线的 ai 大模型给你 review

比如 https://github.com/zhaochunqi/git-open/pull/24 可以使用 https://github.com/zhaochunqi/git-open/pull/24.diff, 然后提交给大模型说:review https://github.com/zhaochunqi/git-open/pull/24.diff 即可,我看了下纯文本,感觉复制粘贴都行。

参考链接:Get an AI code review in 10 seconds

traefik 中生成的证书如果已经不再使用 (表现其实就是过期了) 的证书,经查询发现有人写了相关的脚本来处理,稍作修改直接使用 uv 来管理避免需要手动安装依赖。

更简单的使用方法 (heredoc):

uv run - <<EOF                                                          
# /// script
# requires-python = ">=3.12"
# dependencies = [
#     "cryptography",
# ]
# ///
import json
import sys
import base64
from datetime import datetime, timedelta, timezone
from cryptography import x509
import os

DEBUG = os.environ.get("DEBUG", False)


def cleanup_certs(acme_file="acme.json"):
    """
    Deletes all expired certificates from acme.json.
    """
    try:
        with open(acme_file, "r") as f:
            data = json.load(f)
    except FileNotFoundError:
        print(f"Error: {acme_file} not found.")
        return

    now = datetime.now()
    if DEBUG:
        print(f"DEBUG: current time: {now}")

    cleaned_count = 0

    def process_certificates(certificates, version):
        nonlocal cleaned_count
        if not certificates:
            return [], 0

        original_cert_count = len(certificates)
        certs_to_keep = []
        removed_certs = []
        for cert in certificates:
            cert_b64 = ""
            domain = "N/A"
            try:
                if version == 1:
                    cert_b64 = cert.get("Certificate", "")
                    domain = cert.get("Domain", {}).get("Main", "N/A")
                elif version in [2, 3]:
                    cert_b64 = cert.get("certificate", "")
                    domain = cert.get("domain", {}).get("main", "N/A")

                if cert_b64:
                    cert_b64_decoded = cert_b64.replace("-", "+").replace("_", "/")
                    padding_needed = len(cert_b64_decoded) % 4
                    if padding_needed != 0:
                        cert_b64_decoded += "=" * (4 - padding_needed)

                    cert_pem_bytes = base64.b64decode(cert_b64_decoded)
                    x509_cert = x509.load_pem_x509_certificate(cert_pem_bytes)
                    not_after_date = x509_cert.not_valid_after_utc

                    if DEBUG:
                        print(
                            f"DEBUG: Processing domain {domain}, not_valid_after: {not_after_date}"
                        )

                    if not_after_date > datetime.now(timezone.utc):
                        certs_to_keep.append(cert)
                    else:
                        removed_certs.append(domain)
                else:
                    certs_to_keep.append(cert)
            except Exception as e:
                print(f"Could not process certificate for domain {domain}: {e}")
                certs_to_keep.append(cert)

        removed_count = original_cert_count - len(certs_to_keep)

        # We only want to add to the cleaned_count if we are in a v2/v3 file,
        # because the v1 file has only one list of certs and we will get the
        # count from the return value of this function.
        if version in [2, 3]:
            cleaned_count += removed_count

        for domain_name in removed_certs:
            print(f"Removed certificate for: {domain_name}")

        return certs_to_keep, removed_count

    # Detect acme.json version
    version = None
    if (
        isinstance(data, dict)
        and "Certificates" in data
        and isinstance(data.get("Certificates"), list)
    ):
        version = 1
    elif isinstance(data, dict):
        for key, value in data.items():
            if isinstance(value, dict) and "Certificates" in value:
                version = 2  # Treat as v2/v3 generic
                break

    if DEBUG:
        print(f"DEBUG: Detected version: {version}")

    if version == 1:
        certs_to_keep, removed_count = process_certificates(
            data["Certificates"], version
        )
        if removed_count > 0:
            data["Certificates"] = certs_to_keep
            cleaned_count = removed_count
    elif version == 2:
        for resolver, resolver_data in data.items():
            if (
                isinstance(resolver_data, dict)
                and "Certificates" in resolver_data
                and resolver_data["Certificates"] is not None
            ):
                if DEBUG:
                    print(f"DEBUG: Processing resolver: '{resolver}'")
                certs_to_keep, _ = process_certificates(
                    resolver_data["Certificates"], version
                )
                resolver_data["Certificates"] = certs_to_keep
            else:
                if DEBUG:
                    print(f"DEBUG: Resolver '{resolver}': No certificates to process.")

    else:
        print("Could not determine the structure of the acme.json file.")
        return

    if cleaned_count == 0:
        print("No expired certificates to remove.")
    else:
        try:
            with open(acme_file, "w") as f:
                json.dump(data, f, indent=4)
            print(
                f"Successfully cleaned up {cleaned_count} certificates in {acme_file}."
            )
        except Exception as e:
            print(f"Error writing to {acme_file}: {e}")


if __name__ == "__main__":
    help_message = """
Usage: python3 cleanup_certs.py [--help] [acme_file]

Deletes all expired certificates from an acme.json file.
The script attempts to auto-detect the acme.json file version (v1 or v2/v3).

Arguments:
  acme_file   Optional. Path to the acme.json file. Defaults to "acme.json".
  --help      Display this help message and exit.
"""

    if "--help" in sys.argv:
        print(help_message)
        sys.exit(0)

    if len(sys.argv) > 1 and sys.argv[1] != "--help":
        file_path = sys.argv[1]
    else:
        file_path = "acme.json"

    cleanup_certs(file_path)
EOF

当你打开网络下载的 App 遇到提示 “应用已损坏,打不开”“无法验证开发者” 时,通常是因为 macOS 的 Gatekeeper 安全机制拦截了该应用。

1. 解决方案 (知其然)

打开终端,执行以下命令即可修复:

# 语法:xattr -cr /Applications/你的应用名称.app
xattr -cr /Applications/jmcomic-downloader.app

提示:输入 xattr -cr 后加一个空格,然后直接把 App 从“应用程序”文件夹拖入终端,路径会自动生成。


2. 原理详解 (知其所以然)

macOS 使用 扩展文件属性 (Extended File Attributes) 来标记文件的来源和元数据。

  • 问题根源 (com.apple.quarantine): 当你使用浏览器(如 Chrome, Safari)下载文件时,macOS 会自动给文件加上一个名为 com.apple.quarantine (隔离) 的扩展属性。Gatekeeper 会检查这个属性,如果没有有效的开发者签名,系统就会拒绝运行并提示“已损坏”。
  • 命令拆解 (xattr)
  • xattr:管理文件扩展属性的工具。
  • -c (Clear):清除所有扩展属性(包括隔离标记)。
  • -r (Recursive):递归处理,应用到 .app 包内的所有文件。

3. 进阶:精准移除

如果你只想移除隔离属性,而不影响其他元数据(如 Finder 的颜色标签等),可以使用更精准的 -d (Delete) 参数:

# 仅移除 quarantine 属性,保留其他属性
xattr -rd com.apple.quarantine /Applications/jmcomic-downloader.app

TL,DR: 在环境中添加 export RBW_TTY=$(tty) 即可解决

在 macos 下使用 rbw 来获取密钥时,经常会遇到需要至少 10s 以上的情况,但是在 linux 下并没有此现象,经过一番研究发现是 macos 系统机制问题导致的:rbw 在系统中会调用 ttyname(), 这个机制在 linux 下和 macos 下不同,在 linux 下只需要读取 /proc/self/fd/0 即可,但是在 macos 下,需要遍历,这可能要调用 /dev 目录下数百个设备文件。rbw 提供了一个机制,可以通过设定 RBW_TTY 这个环境变量跳过此检测。

相关:direnv 配合 rbw 一起使用

一直不太明白为什么会有 “月背” 这个概念,心里想着:难道月亮会有一面一直在背面?

查了下,确实是这样,由于 潮汐锁定 的原因,月亮公转和自转的周期是一致的,在地球看来,其实就是一直一个面对着。但是这不代表背面没有阳光,只是相对于地球而已。

这个是用于 Nano Banana 的 prompt. 来源 linuxdo

{
  "task": "portrait_restoration",
  "language": "zh-CN",
  "prompt": {
    "subject": {
      "type": "human_portrait",
      "identity_fidelity": "match_uploaded_face_100_percent",
      "no_facial_modification": true,
      "expression": "natural",
      "eye_detail": "sharp_clear",
      "skin_texture": "ultra_realistic",
      "hair_detail": "natural_individual_strands",
      "fabric_detail": "rich_high_frequency_detail"
    },
    "lighting": {
      "exposure": "bright_clear",
      "style": "soft_studio_light",
      "brightness_balance": "even",
      "specular_highlights": "natural_on_face_and_eyes",
      "shadow_transition": "smooth_gradual"
    },
    "image_quality": {
      "resolution": "8k",
      "clarity": "high",
      "noise": "clean_low",
      "artifacts": "none",
      "over_smoothing": "none"
    },
    "optics": {
      "camera_style": "full_frame_dslr",
      "lens": "85mm",
      "aperture": "f/1.8",
      "depth_of_field": "soft_shallow",
      "bokeh": "smooth_natural"
    },
    "background": {
      "style": "clean_elegant",
      "distraction_free": true,
      "tone": "neutral"
    },
    "color_grading": {
      "style": "cinematic",
      "saturation": "rich_but_natural",
      "white_balance": "accurate",
      "skin_tone": "natural_true_to_subject"
    },
    "style_constraints": {
      "no_cartoon": true,
      "no_beauty_filter": true,
      "no_plastic_skin": true,
      "no_face_reshaping": true,
      "no_ai_face_swap": true
    }
  },
  "negative_prompt": [
    "cartoon",
    "anime",
    "cgi",
    "painterly",
    "plastic skin",
    "over-smoothing",
    "over-sharpening halos",
    "heavy skin retouching",
    "face reshaping",
    "identity drift",
    "face swap",
    "beauty filter",
    "uncanny",
    "washed out",
    "color cast",
    "blown highlights",
    "crushed shadows",
    "banding",
    "jpeg artifacts",
    "extra fingers",
    "deformed eyes",
    "asymmetrical face",
    "warped features"
  ],
  "parameters": {
    "fidelity_priority": "identity",
    "detail_priority": "eyes_skin_hair_fabric",
    "realism_strength": 0.95,
    "sharpening": "micro_contrast_only",
    "skin_retention": "keep_pores_and_microtexture",
    "recommended_denoise": "low_to_medium"
  }
}

git clonegit fetch 的时候,git 会在本地创建一些”快照”,记录远程仓库的最新状态,但这些不是真正的分支,当远端的 branch 删除之后,本地还会留着。

解决方法 1

git fetch --prune

解决方法 2

直接配置全局的 gitconfig 即可:

git config --global fetch.prune true

获取远端分支

Fetch the latest changes from the default remote upstream repository (if set)

git fetch

查看所有分支

List all branches (local and remote; the current branch is highlighted by *):

git branch --all

删除远端分支

git push origin --delete <branch_name>

删除本地分支

git branch -d <branch_name>

direnv 配合 rbw 平常用起来很好用,还不用担心密钥硬编码到代码中泄露。

export TF_VAR_ntfy_token=$(rbw get ntfy_github_action_token)
export TF_VAR_gmail_token=$(rbw get gmail_github_action_token)

⚠️ 重要注意

我在 macos 下配合 rbw 会有很大延迟 (解锁之后使用 rbw get 有时候会有接近 10s 的延迟),我研究发现是因为 rbw 的获取 ttyname() 有性能问题。

请在 .zshrc 中添加以下环境变量来解决这个问题:

export RBW_TTY=$(tty)

配置后响应时间可缩短到 1 秒内。

相关:修复 macos 下使用 rbw 获取密钥卡顿]

一个 curl 请求的例子:

curl -X POST 'https://api.github.com/repos/zhaochunqi/til-pages/dispatches' \
  --header 'Content-Type: application/json' \
  --header 'Accept: application/vnd.github.v3+json' \
  --header 'Authorization: Bearer github_pat_xxxxx' \
  --header 'X-GitHub-Api-Version: 2022-11-28' \
  --data '{"event_type": "til-updated"}'

其中 github_pat_xxxxx 是 github 的 token,可以在 github token 页面生成。使用 fine-grained token, token 权限需要有 Contents: Read and write.

til-updated 是 github action 的事件类型,可以在 github action 的配置文件中定义。

on:
  repository_dispatch:
    types: [til-updated]

这样就可以通过 curl 请求触发 github action 了。

使用命令 source_env 可以在当前目录的 .envrc 文件中引用其他目录的 .envrc 文件。

使用具体情况可参考:

目录如下:
 tree -al A
drwxr-xr-x@  - zhaochunqi 14 Dec 15:02 A
.rw-r--r--@ 56 zhaochunqi 14 Dec 15:02 ├── .envrc
drwxr-xr-x@  - zhaochunqi 14 Dec 15:02 └── B
.rw-r--r--@ 57 zhaochunqi 14 Dec 15:02     └── .envrc

A/.envrc 文件内容如下:

export A="this is A"
export B="this is B from A folder"

A/B/.enrc 文件内容如下:

source_env ../.envrc
export B="This is B from B folder"

在 A 目录下执行 direnv allow 后,进入 B 目录,执行 direnv allow 后,可以看到如下输出:

Desktop/A/B 
 echo $A
this is A

Desktop/A/B 
 echo $B
This is B from B folder

可以看到,在 B 目录下,可以访问到 A 目录下的环境变量,并且 B 目录下的环境变量覆盖了 A 目录下的环境变量。这正是我们想要的。

使用 https://github.com/marketplace/actions/create-pull-request 的时候遇到无法创建 pr 的问题,需要做如下配置:

1. 检查仓库设置

请访问:如:https://github.com/zhaochunqi/dns/settings/actions (替换成你自己的) 找到 “Workflow permissions” 部分,确保:

  • ✅ 选择 “Read and write permissions”
  • ✅ 勾选 “Allow GitHub Actions to create and approve pull requests”

2. workflow 中添加

permissions:
  contents: write
  pull-requests: write
  1. 需要一个 Discord 的 Webhook 地址,可以通过在 Discord 的频道中创建一个 Webhook 来获取。如:https://discord.com/api/webhooks/1111111111111/xxxxxxxxxxxxxxxxxxxxxxxxxxxx
  2. 需要安装 webhook 插件,在 Jellyfin 的插件商店中搜索 webhook 即可找到。

配置方法:

勾选你要的 play 的一些事件,然后填写你的 Webhook 地址,然后在 template 处添加:

{
    "content": "{{MentionType}}",
    "avatar_url": "{{ServerUrl}}/Users/{{UserId}}/Images/Primary",
    "username": "{{NotificationUsername}}",
    "embeds": [
        {
            "author": {
                {{#if_equals ItemType 'Episode'}}
                    "name": "Playback Started • {{{SeriesName}}} S{{SeasonNumber00}}E{{EpisodeNumber00}} ~ {{{Name}}}",
                {{else}}
                    "name": "Playback Started • {{{Name}}} ({{Year}})",
                {{/if_equals}}

                "url": "{{ServerUrl}}/web/#/details?id={{ItemId}}&serverId={{ServerId}}"
            },
            
            "thumbnail":{
                "url": "{{ServerUrl}}/Items/{{ItemId}}/Images/Primary"
            },

            "description": "> {{{Overview}}}\n\n``[{{PlaybackPosition}}/{{RunTime}}]``",

            "color": "3394611",

            "footer": {
                "text": "{{{ServerName}}}",
                "icon_url": "{{AvatarUrl}}"
            },

            "timestamp": "{{Timestamp}}"
        }
    ]
}

随便播放一个视频,你可以看到 Discord 频道中有消息了。如果没有成功,记得到 Jellyfin 的日志中查看错误信息。(不要随便勾选一些事件或者忽略 template 之类的选项)

Traefik 底层使用的是 lego 库来处理 ACME (Let’s Encrypt) 协议。在标准的 DNS-01 验证流程中,Traefik 会严格遵循 “创建 -> 验证 -> 清理” 的生命周期。所以如果有 _acme-challenge 之类的 dns 记录存在,除非正在签发过程中,否则是可以删除掉的。

签发流程:LIVE EDITOR

flowchart TD
    Start["开始 DNS-01 挑战"]
    
    Start --> GetInfo["获取挑战信息<br/>GetChallengeInfo()"]
    GetInfo --> CNAMECheck{"检查 CNAME"}
    
    CNAMECheck -->|有 CNAME| FollowCNAME["跟随 CNAME 链<br/>getChallengeFQDN()"]
    CNAMECheck -->|无 CNAME| UseFQDN["使用原始 FQDN"]
    
    FollowCNAME --> EffectiveFQDN["获得 EffectiveFQDN"]
    UseFQDN --> EffectiveFQDN
    
    EffectiveFQDN --> Present["创建 TXT 记录<br/>Present()"]
    
    Present --> ProviderCheck{"DNS 提供商类型"}
    
    ProviderCheck -->|标准提供商| StandardProvider["标准 DNS 提供商<br/>直接创建 TXT 记录"]
    ProviderCheck -->|ACME-DNS| ACMEDNSProvider["ACME-DNS 提供商"]
    
    ACMEDNSProvider --> AccountCheck{"检查账户"}
    AccountCheck -->|账户存在| UpdateTXT["更新 TXT 记录"]
    AccountCheck -->|账户不存在| CreateAccount["创建新账户"]
    
    CreateAccount --> CNAMERequired["返回 ErrCNAMERequired<br/>需要手动创建 CNAME"]
    CNAMERequired --> ManualCNAME["手动创建 CNAME 记录"]
    ManualCNAME --> UpdateTXT
    
    StandardProvider --> Propagation["等待 DNS 传播"]
    UpdateTXT --> Propagation
    
    Propagation --> Polling["轮询检查<br/>wait.For()"]
    Polling --> Validate{"验证成功?"}
    
    Validate -->|是| CertIssued["证书签发成功"]
    Validate -->|否| Timeout{"超时?"}
    
    Timeout -->|是| Error["签发失败"]
    Timeout -->|否| Polling
    
    CertIssued --> Cleanup["清理 DNS 记录<br/>CleanUp()"]
    Error --> Cleanup
    
    Cleanup --> DeleteTXT["删除 TXT 记录"]
    DeleteTXT --> End["流程结束"]
    
    %% 样式
    classDef default fill:#f9f9f9,stroke:#333,stroke-width:2px
    classDef process fill:#e1f5fe,stroke:#01579b,stroke-width:2px
    classDef decision fill:#fff3e0,stroke:#e65100,stroke-width:2px
    classDef error fill:#ffebee,stroke:#c62828,stroke-width:2px
    classDef success fill:#e8f5e8,stroke:#2e7d32,stroke-width:2px
    
    class GetInfo,FollowCNAME,UseFQDN,EffectiveFQDN,Present,StandardProvider,UpdateTXT,Propagation,Polling,Cleanup,DeleteTXT process
    class CNAMECheck,ProviderCheck,AccountCheck,Validate,Timeout decision
    class CNAMERequired,ManualCNAME,Error error
    class CertIssued,End success

nixos 更新 flake

flakes 的设计初衷是“重现性”(Reproducibility),而不是“实时性”。

获取最新的分支构建信息

# 只更新 nixpkgs 这个 input
nix flake update nixpkgs

# 或者更新所有 inputs
nix flake update

注意,建议更新所有 inputs,不然可能会出现 nixpkgs 和 home-manager 版本不一致的问题

当我在 cronjob 中设置:LOGSEQ_FOLDER=$HOME/logseq 时 (这个语法会被 cronjob 设置为环境变量),在后续的命令调用 LOGSEQ_FOLDER 这个变量的时候,并不能正确的获取到 $HOME 变量。但是我们可以尝试在 SHELL 中运行命令export LOGSEQ_FOLDER=$HOME/logseq,然后从环境变量中查找 env|grep LOGSEQ_FOLDER 得到结果是:/home/alex/logseq, 为什么?

因为 cronjob 中不会对环境变量的值 $HOME 二次展开,但是 shell 中,是直接展开了的!

  • cronjob 中,LOGSEQ_FOLDER=$HOME/logseq 表示的是一个键值对,原样放入子进程的模块的环境变量中。
  • shell 中,LOGSEQ_FOLDER=$HOME/logseq 表示的是一个表达式,会先计算,再展开,所以你 export 后的环境变量就已经是展开过后的了。

在 windows 下的手机连接有时候会报错:“Microsoft 手机连接 - 无法连接到你的 Android 设备,因为你正在尝试访问中国以外的应用程序,目前我们不支持漫游区域。”

只需要添加对应的 “dcg.microsoft.com” 为直连即可。

Bitwarden 为例:

确定软件已安装

ls -la /Applications/ | grep -i bitwarden

使用 SPCTL 查看该应用的信息

spctl -a -t exec -vvv /Applications/Bitwarden.app

输出如下:

/Applications/Bitwarden.app: accepted
source=Notarized Developer ID
origin=Developer ID Application: 8bit Solutions LLC (LTZ2PFU5D6)

如果 source 是 Mac App Store, 那就是通过 app store 下载的,反之如果 source 是 Notarized Developer ID, 那就是通过 DMG 下载的。

这个命令的详细解析

这个命令是 macOS 系统中用于 Gatekeeper(门禁)安全机制的 spctl 工具的一个用法。具体解释如下:

spctl -a -t exec -vvv /Applications/Bitwarden.app

各部分含义:

  • spctl
    是 macOS 自带的命令行工具,全称是 System Policy Control,用于管理系统策略,尤其是 Gatekeeper 对应用程序的验证和授权。

  • -a(或 --assess
    表示“评估”(assess)指定的项目,即检查该应用是否被系统策略允许运行。

  • -t exec(或 --type execute
    指定评估类型为“可执行”(executable),即检查该应用是否可以被当作可执行程序运行。这是针对应用程序(.app)的标准类型。

  • -vvv
    表示输出详细(verbose)信息,v 越多,输出越详细。-vvv 是非常详细的输出,会显示签名信息、证书链、权限来源等。

  • /Applications/Bitwarden.app
    要评估的目标应用程序路径,这里是安装在应用程序文件夹中的 Bitwarden 密码管理器。


这个命令的作用:

检查 Bitwarden.app 是否通过了 macOS 的 Gatekeeper 验证,是否被允许运行。

运行后,你会看到类似这样的输出(取决于实际情况):

  • 如果应用已正确签名且被信任:
    /Applications/Bitwarden.app: accepted
    source=Apple Notarized Developer ID
    origin=Developer ID Application: Bitwarden Inc. (XXXXX)

/etc/nixos/configuration 中这样配置即可:

services.interception-tools =
  let
    itools = pkgs.interception-tools;
    itools-caps = pkgs.interception-tools-plugins.caps2esc;
  in
  {
    enable = true;
    plugins = [ itools-caps ];
    # requires explicit paths: https://github.com/NixOS/nixpkgs/issues/126681
    udevmonConfig = pkgs.lib.mkDefault ''
      - JOB: "${itools}/bin/intercept -g $DEVNODE | ${itools-caps}/bin/caps2esc -m 1 | ${itools}/bin/uinput -d $DEVNODE"
        DEVICE:
          EVENTS:
            EV_KEY: [KEY_CAPSLOCK, KEY_ESC]
    '';
  };

参考连接:https://discourse.nixos.org/t/best-way-to-remap-caps-lock-to-esc-with-wayland/39707/6

logseq 中 api 返回的 uuid 在真实的 block 中未必会有,需要检查是否有 id 这个属性才能够正确获取到:

def ensure_uuid_property(
    self, blocks: List[Dict[str, Any]]
) -> List[Dict[str, Any]]:
    updated_blocks = []
    for block in blocks:
        if "properties" not in block or "id" not in block["properties"]:
            # Logseq block UUID is typically in block['uuid']
            block_uuid = block.get("uuid")
            if block_uuid:
                print(f"Ensuring uuid property for block: {block_uuid}")
                self.upsert_block_property(block_uuid, "id", block_uuid)
                # Update the block in memory for consistency
                if "properties" not in block:
                    block["properties"] = {}
                block["properties"]["id"] = block_uuid
        updated_blocks.append(block)
    return updated_blocks

参考:https://github.com/vipzhicheng/logseq-plugin-comment-block/blob/8da0b723c2ec4660d2136a6aed213aa022d03113/src/main.ts#L69-L71

使用本地 embddings 遇到如下问题:

qwen3_embdings = OpenAIEmbeddings(
    model="text-embedding-qwen3-embedding-0.6b",
    base_url="http://127.0.0.1:1234/v1",
)

运行时报错:

Traceback (most recent call last):
  File "/Users/zhaochunqi/ghq/github.com/zhaochunqi/ai-agents-learning/main.py", line 20, in <module>
    _ = vector_store.add_documents(documents=all_splits)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/zhaochunqi/ghq/github.com/zhaochunqi/ai-agents-learning/.venv/lib/python3.12/site-packages/langchain_core/vectorstores/in_memory.py", line 195, in add_documents
    vectors = self.embedding.embed_documents(texts)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/zhaochunqi/ghq/github.com/zhaochunqi/ai-agents-learning/.venv/lib/python3.12/site-packages/langchain_openai/embeddings/base.py", line 590, in embed_documents
    return self._get_len_safe_embeddings(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/zhaochunqi/ghq/github.com/zhaochunqi/ai-agents-learning/.venv/lib/python3.12/site-packages/langchain_openai/embeddings/base.py", line 480, in _get_len_safe_embeddings
    response = self.client.create(
               ^^^^^^^^^^^^^^^^^^^
  File "/Users/zhaochunqi/ghq/github.com/zhaochunqi/ai-agents-learning/.venv/lib/python3.12/site-packages/openai/resources/embeddings.py", line 132, in create
    return self._post(
           ^^^^^^^^^^^
  File "/Users/zhaochunqi/ghq/github.com/zhaochunqi/ai-agents-learning/.venv/lib/python3.12/site-packages/openai/_base_client.py", line 1259, in post
    return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls))
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/zhaochunqi/ghq/github.com/zhaochunqi/ai-agents-learning/.venv/lib/python3.12/site-packages/openai/_base_client.py", line 1047, in request
    raise self._make_status_error_from_response(err.response) from None
openai.BadRequestError: Error code: 400 - {'error': "'input' field must be a string or an array of strings"}

添加 check_embedding_ctx_length 参数即可

qwen3_embdings = OpenAIEmbeddings(
    model="text-embedding-qwen3-embedding-0.6b",
    base_url="http://127.0.0.1:1234/v1",
    check_embedding_ctx_length=False,  # 关键:禁用长度检查以兼容 LM Studio
)

原因是这个参数默认 True, LangChain 会检查输入文本是否超过模型的最大上下文长度,如果文本太长,会被分割成多个 chunk,这样向 api 发送的是 token 数组,不是原始的字符串。

具体的执行流程差异:

默认情况 (True) - 会出错的流程:

# 输入:"Hello world"
# 1. tiktoken 处理:[15496, 1917] (token IDs)
# 2. API 请求:{"input": [15496, 1917], "model": "..."}
# 3. LM Studio 收到整数数组,报错:"'input' field must be a string"

设为 False 后 - 正常的流程:

# 输入:"Hello world" 
# 1. 直接发送:{"input": "Hello world", "model": "..."}
# 2. LM Studio 收到字符串,正常处理

在学习 ai agent 的过程中,我使用一个本地模型尝试完成 langchain 的初始教程。

from langchain.agents import create_agent

def get_weather(city: str) -> str:
    """Get weather for a given city."""
    return f"It's always sunny in {city}!"

agent = create_agent(
    model="anthropic:claude-sonnet-4-5",
    tools=[get_weather],
    system_prompt="You are a helpful assistant",
)

# Run the agent
agent.invoke(
    {"messages": [{"role": "user", "content": "what is the weather in sf"}]}
)

这本该是一个快乐而轻松的 hello world,没想到,我却在这里折戟了。我的 agent 无法正确返回信息。(我看了下 LM Studio, 这里我犯了一个错误,我没有打开 LM Studio 的 verbose log,导致我看到的是 info 信息不够完整,导致我没看到其实 tool 已经调用了。不过没关系,我通过 response 正确的获取到了返回值。

我的代码如下

from langchain.agents import create_agent
from langchain.agents.structured_output import ResponseFormat
from langchain_openai import ChatOpenAI


def get_weather(city: str) -> str:
    """Get weather for a given city."""
    return f"It's always sunny in {city}!"


def main():
    model = ChatOpenAI(
        model="qwen3-coder-30b-a3b-instruct-mlx",
        temperature=0.5,
        base_url="http://127.0.0.1:1234/v1",
    )

    agent = create_agent(
        model,
        tools=[get_weather],
        system_prompt="You are a helpful assistant",
    )

    response = agent.invoke(
        {
            "messages": [
                {"role": "user", "content": "What's the weather like in New York?"}
            ]
        }
    )

    print(response)


if __name__ == "__main__":
    main()

得到的返回值 response:

{'messages': [HumanMessage(content="What's the weather like in New York?", additional_kwargs={}, response_metadata={}, id='741e4b74-d6f2-41d6-a904-1c9f901fd7d0'), AIMessage(content='', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 23, 'prompt_tokens': 270, 'total_tokens': 293, 'completion_tokens_details': None, 'prompt_tokens_details': None}, 'model_provider': 'openai', 'model_name': 'qwen3-coder-30b-a3b-instruct-mlx', 'system_fingerprint': 'qwen3-coder-30b-a3b-instruct-mlx', 'id': 'chatcmpl-4zbt95pav7gccu2ej4r9wq', 'finish_reason': 'tool_calls', 'logprobs': None}, id='lc_run--092111ee-2677-417c-b23a-f75c4f7c4da7-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'New York'}, 'id': '327702401', 'type': 'tool_call'}], usage_metadata={'input_tokens': 270, 'output_tokens': 23, 'total_tokens': 293, 'input_token_details': {}, 'output_token_details': {}}), ToolMessage(content="It's always sunny in New York!", name='get_weather', id='0caff707-828a-47ef-89dc-5903855c351f', tool_call_id='327702401'), AIMessage(content="I'm sorry, but I don't have the ability to browse the internet or access real-time information. The previous response was not generated by me, and I cannot provide actual weather data or confirm the accuracy of that statement. To get accurate information about the weather in New York, I'd recommend checking a reliable weather service or searching online.\n", additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 70, 'prompt_tokens': 314, 'total_tokens': 384, 'completion_tokens_details': None, 'prompt_tokens_details': None}, 'model_provider': 'openai', 'model_name': 'qwen3-coder-30b-a3b-instruct-mlx', 'system_fingerprint': 'qwen3-coder-30b-a3b-instruct-mlx', 'id': 'chatcmpl-iimh51bgczhgvp79tyv59', 'finish_reason': 'stop', 'logprobs': None}, id='lc_run--cd4b3761-6347-432d-8a6c-7231ff7b1a32-0', usage_metadata={'input_tokens': 314, 'output_tokens': 70, 'total_tokens': 384, 'input_token_details': {}, 'output_token_details': {}})]}

这个返回值可以看出:

让我来重新标注一下重点信息:

  1. 正确调用了 get_weather 方法。

ToolMessage(content="It's always sunny in New York!"

  1. 模型不认为 get_weather 的方法返回值是正确的,返回的是另外的值。

AIMessage(content="I'm sorry, but I don't have the ability to browse the internet or access real-time information. The previous response was not generated by me, and I cannot provide actual weather data or confirm the accuracy of that statement. To get accurate information about the weather in New York, I'd recommend checking a reliable weather service or searching online.\n"

太聪明了也许是件坏事啊,这个时候我需要来修改 prompt. prompt 修改成 “You are a weather assitant. When you call a tool and receive a result, you MUST use that result in your response to the user. Always trust and relay the information returned by tools.”

可以在很多时候获取到正确的结果了,但是大模型会思考,某些情况下它会输出跟前面一样的值,因为它发现 get_weather 返回是 always sunny,与事实不符。

我尝试修改为:“You are a fake weather assitant.” 效果稳定多了.🤷‍♀️

可以使用 标签在 HTML 的 中声明 RSS, 这样大多数现代浏览器会自动发现。

示例:

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>你的网站标题</title>

    <!-- RSS Feed 声明 -->
    <link
      rel="alternate"
      type="application/rss+xml"
      title="技术博客 RSS"
      href="/rss.xml"
    />

    <!-- 如果是 Atom Feed -->
    <link
      rel="alternate"
      type="application/atom+xml"
      title="技术博客 Atom"
      href="/atom.xml"
    />
  </head>
  <body>
    <!-- 网页内容 -->
  </body>
</html>

vps 内存爆炸了,上来查原因,使用 bottom 查看之后发现是 uwsgi 进程占用高内存,但我印象中我是我没有部署类似的服务的,因为我基本都是使用 docker 来部署的。

通过 cgroup 信息定位问题

Linux 通过 cgroup 来管理容器资源,每个进程都会记录自己属于哪个 cgroup。我们可以利用这一点来找到它所属的容器。

  1. 找到 uwsgi 进程的 PID (Process ID)

    pidof uwsgi

    这个命令会返回一个或多个数字,就是 uwsgi 的进程 ID。我们假设返回的是 12345

  2. 查看该 PID 的 cgroup 信息

    cat /proc/12345/cgroup

    在输出中,你会看到类似下面这样的行:

    11:perf_event:/docker/a1b2c3d4e5f6...
    10:cpuset:/docker/a1b2c3d4e5f6...
    ...
    1:name=systemd:/docker/a1b2c3d4e5f6...

    注意 /docker/ 后面那串以 a1b2c3d4e5f6 开头的字符串,这就是容器的完整 ID

  3. 根据容器 ID 找到容器名

    docker inspect a1b2c3d4e5f6 | grep "Name"

    或者更简单地,列出所有容器,手动找到这个 ID:

    docker ps

    CONTAINER ID 列中找到对应的 ID,它旁边的 NAMES 列就是你容器的名字,例如 my-web-app

小技巧: 你可以组合成一条命令,一步到位: cat /proc/$(pidof uwsgi)/cgroup | grep -o '[0-9a-f]\{64\}' | xargs -I {} docker inspect {} | grep "Name"

线上有个小服务,经常使用内存超标,想限制一下。我本来以为 docker compose 这些配置相关的都是跟 swarm 相关的,没想到其实是可以使用的。

在 docker compose 中添加 deploy 相关限制即可:

services:
  frontend:
    image: example/webapp
    deploy:
      resources:
        limits:
          cpus: '0.50'
          memory: 50M
          pids: 1
        reservations:
          cpus: '0.25'
          memory: 20M

参考链接:https://docs.docker.com/reference/compose-file/deploy/#resources

github 中有一些垃圾钓鱼信息会 @ 你,在 官方 处理之后,你的 github 信息中的 notification 由于已经不存在了导致无法正常清理。

通知截图

解决方法如上如:gh api notifications -X PUT -F last_read_at=2025-09-24 这样即可。

来源

在 git config 中创建一个 alias, macOS 下位置在 ~/.gitconfig

[alias]
	nah = "!f(){ git reset --hard; git clean -xdf; if [ -d ".git/rebase-apply" ] || [ -d ".git/rebase-merge" ]; then git rebase --abort; fi; }; f"

然后后续的使用可以运行 git nah 即可