这个blog两年没更新过了啊!靠着AI打气又拾起来了!
起因是节前Fusion 360警告我win10不受支持了,Gemini也建议我换个系统,并且疯狂安利Win11,安抚我说Win11已经不是几年前那种费拉不堪的状态了。
再信AI一次。
0 备份
尤其是wsl2!:
mkdir -p ~/wsl_backup/manifestscd ~/wsl_backup/manifests
######## apt ######### 导出所有手动安装的包(排除依赖包)comm -23 <(apt-mark showmanual | sort -u) <(gzip -dc /var/log/installer/initial-status.gz | sed -n 's/^Package: //p' | sort -u) > apt-manual-packages.txt# 如果上面命令报错或太复杂,用这个简单的版本(会包含一些基础包):apt-mark showmanual > apt-manual-list.txt
#################### python 和 conda ##################### 激活主环境,比如 base 或 pytorchconda env export --no-builds > conda_environment.ymlpip freeze > requirements.txt
######### rust #########cargo install --list > cargo-crates.txt
######### node #########npm list -g --depth=0 > npm-global-packages.txt
############### shell conf ###############cd ~tar -czvf ~/wsl_backup/dotfiles.tar.gz \ .zshrc \ .bashrc \ .profile \ .zsh_history \ .bash_history .ssh \ .tmux.conf \ .fzf.zsh \ .gitconfig \ .vimrc \ .config/nvim \ .gnupg \ .condarc \ .pip \ .cargo/config.toml \1 装系统
装专业版即可,需要注意,分区别在跟Linux似的一块/用到底,2T盘可以先给C盘384G(),剩下的给D盘。使用的时候:
- wsl2放D盘
- 尽量用protable软件,全放D盘,那些死皮赖脸要待在C盘的软件再放C盘。
如此,重装的时候不会一锅端。
另外,装好了第一次见到Windows初始化配置的时候,微软强制登录Microsoft账号。此时Shift + F10,输入OOBE\BYPASSNRO自动重启,就可以选“我没有 Internet 连接”跳过账号登录。好处是可以完全自己决定user文件夹的名字。
进了系统先装驱动,尤其是显卡驱动,先装完驱动再装wsl2,但是主板带的什么MSI Center装完驱动可以卸了。
NVIDIA APP 选 NVIDIA studio 驱动,对跑CUDA的场景来说,Game Ready 驱动更新过于频繁。
2 清理微软的垃圾
第一时间防火防盗防微软。
-
彻底禁止 Windows 自动更新驱动:
Win+R输入gpedit.msc:计算机配置 -> 管理模板 -> Windows 组件 -> Windows 更新 -> 管理从 Windows 更新提供的更新,右侧的 “Windows 更新不包括驱动程序”,选择 “已启用”。如此,让 Windows Update 只推安全补丁和功能更新,绝不会动显卡、网卡、声卡驱动。 -
保留 Defender,配置“码农豁免区” 打开 Windows 安全中心 -> 病毒和威胁防护 -> 管理设置,下拉找到 “排除项” -> 添加或删除排除项,酌情加入:
D:\WSL(放 WSL 镜像,防止扫描 vhdx 读写);D:\DEV(放代码);D:\MOEDL(放模型);- etc.
-
左下角“天气新闻”等等是 小组件” (Widgets):
Win+R输入gpedit.msc:计算机配置 -> 管理模板 -> Windows 组件 -> 小组件,右侧的 “允许小组件”,选择 “已禁用”。 -
锁屏时“天气新闻”等待是 Windows 聚焦 (Windows Spotlight) :
- 右键桌面 -> “个性化” -> 选择 “锁屏界面”,在个 性化锁屏界面 中选择 “图片” (Picture) 。
- 再往下看,如果有 “在锁屏界面上获取花絮、提示、技巧和更多内容”,选择 关闭。
- 如果有 “锁屏界面状态”,改为 无 (None) 。
-
开始菜单里的垃圾,右键桌面 -> “个性化” -> “开始”:
- 显示最近添加的应用 -> 关。
- 显示最常用的应用 -> 关。
- 在“开始”菜单中……显示最近打开的项目 -> 关
-
开始菜单的搜索 强行把本地搜索和 Bing 搜索混合:
Win + R输入gpedit.msc:用户配置 (User Configuration) -> 管理模板 (Administrative Templates) -> Windows 组件 (Windows Components) -> 文件资源管理器 (File Explorer),右侧找:“关闭文件资源管理器搜索框中的最近搜索条目显示” (Turn off display of recent search entries in the Windows Search box) ,选择 “已启用” (Enabled) 。重启资源管理器即可,taskkill /f /im explorer.exe; start explorer.exe。注意:微软经常改这个策略的名字,比如也可能叫“关闭搜索框建议”。- 也可以执行
reg add "HKCU\Software\Policies\Microsoft\Windows\Explorer" /v "DisableSearchBoxSuggestions" /t REG_DWORD /d 1 /f修改。 - 关闭 “搜索要点” (Search Highlights):设置 -> 隐私和安全性 -> 搜索权限,往下找到 “更多设置”,关掉 “显示搜索要点” (Show search highlights) 。
-
任务栏的搜索框,任务栏空白处 右键 -> “任务栏设置”,第一项就是 “搜索”,选择 “隐藏”。
-
Steam/Epic党可以把XBox相关都端了:
- 关闭 Xbox Game Bar:在 设置 -> 游戏 -> Xbox Game Bar,关闭 “允许控制器打开 Game Bar”。
- 开始菜单搜 “Xbox”,右键 -> 卸载。
- 唯一例外:游戏模式 (Game Mode) ,留着,在 设置 -> 游戏 -> 游戏模式 里,保持 开 (ON)。
-
关闭 OneDrive:
- 解除“文件夹备份”,任务栏右下角的那个“云朵”图标,右上角齿轮进入 设置,找到 “同步并备份” (Sync and backup) -> “管理备份” (Manage backup),把“文档”、“图片”、“桌面”全部关掉(Stop backup)。,此时系统会把这些文件夹的路径从
C:\Users\<username>\OneDrive\Desktop恢复为C:\Users\<username>\Desktop - 取消链接:设置 里 -> “账户” (Account),点击 “取消链接此电脑” (Unlink this PC)。
- 卸载:设置 -> 应用 -> 安装的应用,搜 “OneDrive” -> 卸载。
- 解除“文件夹备份”,任务栏右下角的那个“云朵”图标,右上角齿轮进入 设置,找到 “同步并备份” (Sync and backup) -> “管理备份” (Manage backup),把“文档”、“图片”、“桌面”全部关掉(Stop backup)。,此时系统会把这些文件夹的路径从
-
至于 基于虚拟化的安全性 (VBS)” / HVCI,模型和网上的说法都没定论。
Win键搜索 “内核隔离” -> “内存完整性”,收益有限,关不关无所谓。- 网上流传的
bcdedit /set hypervisorlaunchtype off不能设置, wsl2 要用。
-
找回Win10右键菜单:
Terminal window reg add "HKCU\Software\Classes\CLSID\{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}\InprocServer32" /f /ve重启资源管理器即可。后悔的话:
Terminal window reg delete "HKCU\Software\Classes\CLSID\{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}" /f
3 清理其他垃圾,装Windows下的常用工具
- 搜狗自带桌面壁纸和汪仔,记得删掉和关闭。
- 取消WPS的云盘资源管理器图标、取消传输助手自启。
- 装cherrystudio。
- 装no machine。
- Snipaste用portable。
- Motrix用portable。
装Git:
winget install --id Git.Git装微信QQ:
winget install Tencent.WeChatwinget install Tencent.QQPotPlayer:
winget install Daum.PotPlayer装powertoys:
winget install Microsoft.PowerToys装Everything和Listary:
winget install voidtools.Everythingwinget install -e --id Bopsoft.Listary4 安装wsl2
有显卡驱动了再装wsl2。
-
先装 WSL 本体,管理员 PowerShell:
Terminal window wsl --install -
装完建议重启,确认 WSL 版本:
Terminal window wsl --version -
在 D 盘准备一个专门目录
D:\WSL,安装一个临时 Ubuntu(会默认在 C 盘),启动一次 Ubuntu,完成用户名/密码初始化后退出。Terminal window wsl --install -d Ubuntu -
把迁移到 D 盘:
- 先关掉 WSL:
Terminal window wsl --shutdown- 导出为 tar(临时文件建议也放 D 盘):
Terminal window wsl --export Ubuntu D:\WSL\ubuntu.tar- 注销原来的 Ubuntu(这一步会删除 C 盘那个实例的注册与磁盘):
Terminal window wsl --unregister Ubuntu- 导入到 D 盘(强制 WSL2):
Terminal window wsl --import Ubuntu D:\WSL D:\WSL\ubuntu.tar --version 2现在 Ubuntu 的 ext4.vhdx 就在
D:\WSL\下面了。 -
wsl2网络配置:建议按老方法,默认NAT,不要配自动代理、DNS隧道等配置。装个clash-verge,用自己写的订阅发布程序让Windows、wsl、PVE、rock5b、o6n等设备同步订阅配置,虽然heavy但是最稳妥:
[wsl2]networkingMode=natautoProxy=falsednsTunneling=falsednsProxy=false新wsl2支持mirror,直接走Windows的网卡,别的没什么问题,但是gemini-cli或codex可能会用不了:
[wsl2]networkingMode=mirroreddnsTunneling=trueautoProxy=truefirewall=true此时wget下载chrome会自动拿到autoproxy配置走clash,但是apt不行,还得手动指定proxy
Terminal window sudo tee /etc/apt/apt.conf.d/99proxy <<'EOF'Acquire::http::Proxy "http://127.0.0.1:7897";Acquire::https::Proxy "http://127.0.0.1:7897";EOF
5 初始化wsl2
建议先改主机名:
sudo hostnamectl set-hostname new_name用gemini和chatgpt写了个一键安装脚本,也可以一步一步执行。
#!/usr/bin/env bashset -e
# ==========================================# WSL2 Development Environment Bootstrap# Target: Debian/Ubuntu# Role: System, Shell, Rust, Node, Python, Git# ==========================================
# 颜色定义,方便看日志GREEN='\033[0;32m'BLUE='\033[0;34m'RED='\033[0;31m'NC='\033[0m'
log() { echo -e "${GREEN}[SETUP] $1${NC}"; }info() { echo -e "${BLUE}[INFO] $1${NC}"; }error() { echo -e "${RED}[ERROR] $1${NC}"; }
# 0. 基础检查# ------------------------------------------if [ "$EUID" -eq 0 ]; then error "请不要使用 root 运行此脚本,请使用普通用户运行!" error "脚本内部会在需要时请求 sudo 权限。" exit 1fi
# 保持 sudo 权限活跃,防止脚本运行中途因为超时等待输入密码sudo -vwhile true; do sudo -n true; sleep 60; kill -0 "$$" || exit; done 2>/dev/null &
# 先把文件建好,后面的 Rust/NVM/Conda 就会以为你已经在使用 zsh 了,# 它们会自动把 export PATH... 写入这两个文件。touch "$HOME/.bashrc"touch "$HOME/.zshrc"
### ========================================### 1. 系统基础 (System Base)### ========================================log "📦 1. 更新系统并安装基础依赖..."
sudo apt update && sudo apt upgrade -y
# 安装必备工具# build-essential: 编译环境# software-properties-common:以此支持 add-apt-repositorysudo apt install -y \ git git-lfs curl wget ca-certificates \ build-essential pkg-config libssl-dev \ zip unzip btop tmux zsh vim \ net-tools iproute2 dnsutils \ jq tree procps make \ direnv rsync shellcheck \ software-properties-common ffmpeg \ fonts-noto-cjk fonts-wqy-microhei \ language-pack-zh-hans
# 注:如果要装yq,直接装go版二进制文件。# 获取并转换系统架构名称 (匹配 mikefarah/yq 的命名规则)ARCH=$(uname -m)case $ARCH in x86_64) YQ_ARCH="amd64" ;; aarch64) YQ_ARCH="arm64" ;; *) echo "不支持的架构"; exit 1 ;;esac
# 下载最新版的二进制文件并安装到 /usr/local/binwget "https://github.com/mikefarah/yq/releases/latest/download/yq_linux_${YQ_ARCH}"sudo mv "yq_linux_${YQ_ARCH}" /usr/local/bin/yqsudo chmod +x /usr/local/bin/yq
# 安装fastfetch,因为sudo后常用 gpg/dirmngr 去连 keyserver,# gpg/dirmngr 不读 apt 的代理配置,# 而且 sudo 默认还会把 shell 里的 http_proxy/https_proxy 环境变量“清掉”,# 导致直连 keyserver,然后被墙阻断,表现为“没反应”。export http_proxy="http://127.0.0.1:7897"export https_proxy="http://127.0.0.1:7897"
# 或永久添加代理echo 'export http_proxy="http://127.0.0.1:7897"' >> ~/.bashrcecho 'export https_proxy="http://127.0.0.1:7897"' >> ~/.bashrcecho 'export http_proxy="http://127.0.0.1:7897"' >> ~/.zshrcecho 'export https_proxy="http://127.0.0.1:7897"' >> ~/.zshrc
sudo --preserve-env=http_proxy,https_proxy add-apt-repository ppa:zhangsongcui3371/fastfetch -ysudo apt updatesudo apt install fastfetch -y
### ========================================### 2. Rust 工具链 (Rust & Modern CLI)### ========================================log "🦀 2. 安装 Rust 环境..."
if ! command -v rustup >/dev/null 2>&1; then # 安装 Rustup curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -yelse info "Rustup 已安装,跳过."fi
# 【关键】加载环境变量,确保 cargo 命令可用# 如果你分步运行,请务必执行这一行source "$HOME/.cargo/env"
log "🦀 安装 Cargo Binstall (加速工具安装)..."if ! command -v cargo-binstall >/dev/null 2>&1; then curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bashfi
log "🦀 安装现代命令行工具 (Rust编写)..."# 以用户token登录方式,解除 GitHub API 速率限制 (Rate Limiting)# cargo-binstall 匿名查询API每小时仅60次,# 不登录超过将报403,登录将提升到5000次# Settings -> Developer settings -> Personal access tokens -> Tokens (classic),# Expiration 选 "No expiration",Scopes (权限) 一个都不用勾export GITHUB_TOKEN="ghp_xxx"
# bob-nvim: Neovim 版本管理器# starship: 跨 Shell 提示符# eza(ls), bat(cat), fd(find), ripgrep(grep), zoxide(cd)cargo binstall -y \ eza bat fd-find ripgrep du-dust procs zoxide hyperfine \ cargo-watch cargo-nextest cargo-update cargo-audit \ starship bob-nvim topgrade tealdeer zellij
# atuin本地编译更不容易报错glibc的错误cargo install atuin
# 装了tealdeer可以卸载 ubuntu 自带的 haskell tldr-hssudo apt remove -y tldr
### ========================================### 3. Neovim (via Bob)### ========================================log "📝 3. 使用 Bob 安装最新 Neovim..."
# 确保 bob 在路径中export PATH="$HOME/.cargo/bin:$PATH"
if command -v bob >/dev/null 2>&1; then # 安装最新的稳定版 bob install stable bob use stable
# 这一步是为了确保 nvim 能被系统找到 # Bob 默认把 nvim 放在 ~/.local/share/bob/nvim-bin if [[ ":$PATH:" != *":$HOME/.local/share/bob/nvim-bin:"* ]]; then info "提示: 请将 $HOME/.local/share/bob/nvim-bin 添加到你的 PATH 中" fielse error "Bob 安装失败,跳过 Neovim 安装。"fi
# 1. 让当前终端立刻生效(临时)export PATH="$HOME/.local/share/bob/nvim-bin:$PATH"# 2. 写入 .bashrc(永久生效,防止下次进 bash 找不到 nvim)echo 'export PATH="$HOME/.local/share/bob/nvim-bin:$PATH"' >> ~/.bashrcecho 'export PATH="$HOME/.local/share/bob/nvim-bin:$PATH"' >> ~/.zshrc
# 如果我觉得 neovim 过于毛坯,可以安装 lazyvim: https://www.lazyvim.org/installation# 备份for dir in ~/.config/nvim ~/.local/share/nvim ~/.local/state/nvim ~/.cache/nvim; do [ -d "$dir" ] && mv "$dir" "${dir}.bak"done
git clone https://github.com/LazyVim/starter ~/.config/nvimrm -rf ~/.config/nvim/.git
# 第一次打开 nvim 要装东西,会比较慢,记得梯子
### ========================================### 4. Node.js (NVM)### ========================================log "🟢 4. 安装 Node.js 环境..."
export NVM_DIR="$HOME/.nvm"
if [ ! -d "$NVM_DIR" ]; then curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.4/install.sh | bashfi
# 【关键】加载 NVM 环境[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
log "🟢 安装 Node LTS..."nvm install --ltsnvm use --ltsnvm alias default lts/*
log "🟢 安装 pnpm 和常用全局包..."if ! command -v pnpm >/dev/null 2>&1; then curl -fsSL https://get.pnpm.io/install.sh | sh -fipnpm setup
# 临时加载 pnpm 路径export PATH="$HOME/.local/share/pnpm:$PATH"
# 安装常用开发工具pnpm add -g typescript tsx eslint prettier @google/gemini-cli @openai/codex# pnpm 交互式构建批准pnpm approve-builds -gcurl -fsSL https://claude.ai/install.sh | bashecho 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc && source ~/.bashrc
### ========================================### 5. Python uv and miniconda### ========================================log "🐍 5. 安装 Python 环境 (uv)..."
if ! command -v uv >/dev/null 2>&1; then curl -Ls https://astral.sh/uv/install.sh | shfi
# 临时加载 uv 路径export PATH="$HOME/.local/bin:$PATH"
log "🐍 安装 Python 工具链..."# uv tool install 类似于 pipx,安装到隔离环境for tool in ruff mypy black poetry httpie; do uv tool install "$tool" || truedone
log "🐍 5.5 安装 Miniconda..."
# 定义安装路径 (通常在 ~/miniconda3 或 ~/.miniconda3)MINICONDA_PATH="$HOME/miniconda3"
if [ ! -d "$MINICONDA_PATH" ]; then # 1. 下载最新的 Linux 安装脚本 wget "https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-$(uname -m).sh" -O miniconda.sh
# 2. 静默安装 (-b: batch mode, -p: prefix path) bash miniconda.sh -b -p "$MINICONDA_PATH"
# 3. 删除安装包 rm miniconda.sh
# 4. 【关键配置】初始化 Shell (bash 和 zsh) # 这步会自动把 conda 的路径写入 .bashrc 和 .zshrc "$MINICONDA_PATH/bin/conda" init bash "$MINICONDA_PATH/bin/conda" init zsh
# 5. 【强力推荐】禁止 Conda 默认激活 base 环境 # 否则每次打开终端前面都有个 (base),会干扰 uv 和系统 Python "$MINICONDA_PATH/bin/conda" config --set auto_activate_base false
log "✅ Miniconda 安装完成 (Base 环境已禁用自动激活)"else info "Miniconda 已存在,跳过安装。"fi
### ========================================### 6. Git & GitHub### ========================================log "🐙 6. 安装 Git 工具..."
# 6.1 GitHub CLI (gh)if ! command -v gh >/dev/null 2>&1; then info "安装 gh cli..." sudo mkdir -p -m 755 /etc/apt/keyrings wget -qO- https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null sudo apt update sudo apt install -y ghfi
# 6.2 Lazygit (自动架构检测)if ! command -v lazygit >/dev/null 2>&1; then info "安装 lazygit..." LAZYGIT_VERSION=$(curl -s "https://api.github.com/repos/jesseduffield/lazygit/releases/latest" | grep -Po '"tag_name": "v\K[^"]*') ARCH=$(uname -m) case $ARCH in x86_64) LG_ARCH="x86_64" ;; aarch64) LG_ARCH="arm64" ;; *) error "不支持的架构: $ARCH"; exit 1 ;; esac
curl -Lo lazygit.tar.gz "https://github.com/jesseduffield/lazygit/releases/download/v${LAZYGIT_VERSION}/lazygit_${LAZYGIT_VERSION}_Linux_${LG_ARCH}.tar.gz" tar xf lazygit.tar.gz lazygit sudo install lazygit /usr/local/bin rm -f lazygit lazygit.tar.gzfi
### ========================================### 7. Shell (Oh-My-Zsh + Starship)### ========================================log "💻 7. 配置 Shell 环境..."
# 安装 OMZif [ ! -d "$HOME/.oh-my-zsh" ]; then info "安装 Oh-My-Zsh..." # --unattended: 不自动进入 zsh # --keep-zshrc: 如果你已经有配置文件,不覆盖 sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended --keep-zshrcfi
# 安装 fzfif [ ! -d "$HOME/.fzf" ]; then info "安装 FZF (命令行模糊搜索)..." git clone --depth 1 https://github.com/junegunn/fzf.git "$HOME/.fzf" # --all: 自动启用 key-bindings (Ctrl+R/T) 和 fuzzy-completion # --no-bash/--no-fish: 我们只配 zsh,不让它乱改其他 shell "$HOME/.fzf/install" --allelse info "FZF 已安装,跳过."fi
ZSHRC="$HOME/.zshrc"
# # 3.1 禁用 OMZ 主题 (让位给 Starship)# # 我们用 sed 查找并替换 ZSH_THEME="..." 为 ZSH_THEME=""# if grep -q 'ZSH_THEME=' "$ZSHRC"; then# sed -i 's/^ZSH_THEME=".*"/ZSH_THEME=""/' "$ZSHRC"# fi
# A. 备份现有的 .zshrc (里面有 Conda 和 FZF 的配置,不能丢!)if [ -f "$ZSHRC" ]; then cp "$ZSHRC" "$ZSHRC.pre-bootstrap" info "已备份现有配置到 .zshrc.pre-bootstrap"fi
# B. 获取 OMZ 官方模板 (作为地基)# 我们把模板下载下来,追加到现有的 .zshrc 后面,这样既保留了 Conda/FZF,又有了 OMZ 的功能if [ ! -f "$HOME/.oh-my-zsh/templates/zshrc.zsh-template" ]; then # 万一模板没下载下来,手动抓取 curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/templates/zshrc.zsh-template > temp_zshrcelse cp "$HOME/.oh-my-zsh/templates/zshrc.zsh-template" temp_zshrcfi
# C. 合并配置 (关键一步!)# 1. 保留 Conda/FZF (在文件头部)# 2. 插入 OMZ 模板 (在中间)# 3. 插入我们自定义的 PATH 和 Tool (在底部)
# 读取现有内容 (Conda + FZF)CURRENT_CONTENT=$(cat "$ZSHRC")
# 覆写 .zshrc:先放 OMZ 模板,再放 原内容cat temp_zshrc > "$ZSHRC"echo "" >> "$ZSHRC"echo "$CURRENT_CONTENT" >> "$ZSHRC"rm temp_zshrc
# D. 修改配置 (现在里面有东西了,可以改了)
# 1. 禁用 OMZ 主题 (让 Starship 接管)# 使用 sed 将 ZSH_THEME="robbyrussell" 替换为 ZSH_THEME=""sed -i 's/^ZSH_THEME=".*"/ZSH_THEME=""/' "$ZSHRC"
# 2. 启用常用插件 (git, extract, sudo)# 这是 OMZ 的精华,必须开sed -i 's/^plugins=(git)/plugins=(git git-extras debian tmux screen history extract colorize web-search docker sudo)/' "$ZSHRC"
# 3. 注入环境变量 (把之前漏在 bashrc 里的补回来)cat <<EOT >> "$ZSHRC"
# --- Bootstrap Generated Config (PATHs & Tools) ---
# 1. PATH 补全 (Rust, Node, Bob, Local Bin)# 注意:Miniconda 已经在文件头部配好了,这里不需要重复export PATH="\$HOME/.local/bin:\$HOME/.cargo/bin:\$HOME/.local/share/bob/nvim-bin:\$HOME/.local/share/pnpm:\$PATH"
. "\$HOME/.cargo/env"
# 2. 工具启动eval "\$(atuin init zsh)" # 启动 atuin (需要在 fzf 初始化之后)eval "\$(starship init zsh)" # 启动 Starshipeval "\$(zoxide init zsh)" # 启动 Zoxide (z 命令)
# 3. 别名 (Aliases)alias cat='bat'alias ls='eza --icons' # 基础 ls 变彩色+图标alias ll='eza -lah --icons --git ' # ll 显示详情、Git状态、隐藏文件alias lt='eza -lah --tree --level=2 --icons' # lt 显示目录树(2层)alias vi='nvim'alias vim='nvim'alias zj='zellij'alias ra='ranger' # 如果装了 ranger
# 4. NVMexport NVM_DIR="\$HOME/.nvm"[ -s "\$NVM_DIR/nvm.sh" ] && . "\$NVM_DIR/nvm.sh" # This loads nvm[ -s "\$NVM_DIR/bash_completion" ] && \. "\$NVM_DIR/bash_completion" # This loads nvm bash_completion
# 5. pnpmexport PNPM_HOME="\$HOME/.local/share/pnpm"case ":\$PATH:" in *":\$PNPM_HOME:"*) ;; *) export PATH="\$PNPM_HOME:\$PATH" ;;esac# pnpm end
EOT
# 1. 删掉顶部的那行 FZF 配置 (使用 sed)#sed -i '/\[ -f ~\/.fzf.zsh \] && source ~\/.fzf.zsh/d' "$ZSHRC"# 2. 把它追加到文件最末尾#echo '[ -f ~/.fzf.zsh ] && source ~/.fzf.zsh' >> "$ZSHRC"
# 修改系统shellchsh -s $(which zsh)
log "✅ .zshrc 修复完成!(Conda + FZF + OMZ + Tools 全部合体)"log "👉 现在输入 'zsh' 进入新环境吧!"
# 安装 ohmytmuxcurl -fsSL "https://github.com/gpakosz/.tmux/raw/refs/heads/master/install.sh#$(date +%s)" | bash
# 安装dockercurl -fsSL https://get.docker.com -o get-docker.shsh get-docker.sh5.1 关于yt-dlp
# 建议使用default安装# 直接`uv tool install yt-dlp --with curl_cffi --with pycryptodomex --upgrade --force`会导致curl_cffi变成最新版# 但yt-dlp可能不支持这么高版本的curl_cffi,而default的extra会自动补齐requests、urllib3、websockets、mutagen、brotli、pycryptodomexuv tool install 'yt-dlp[default,curl-cffi]' --upgrade --force
# 记得装ffmpeg和nvm node,配置node变量mkdir -p ~/.config/yt-dlp && cat <<EOF >> ~/.config/yt-dlp/config--js-runtimes node--remote-components ejs:githubEOFarm设备可能会遇到curl_cffi安装了也没有伪装的问题,转回x86平台吧,暂时无解,像这样:
yt-dlp --list-impersonate-targets[info] Available impersonate targetsClient OS Source--------------------------------------------Tor - curl_cffi>=0.11 (unavailable)Edge - curl_cffi (unavailable)Firefox - curl_cffi>=0.10 (unavailable)Safari - curl_cffi (unavailable)Chrome - curl_cffi (unavailable)5.2 关于恢复备份的.ssh文件夹
拷过去权限得改对
chmod 700 ~/.sshchmod 600 ~/.ssh/id_*chmod 644 ~/.ssh/*.pub# 如果有chmod 600 ~/.ssh/config5.3 关于nomachine
安装时如果遇到60%卡住终端没响应,关掉重开一个终端修复一下安装即可,其实是已经装好,之是post install脚本可能有问题挂起了:
sudo killall apt apt-get dpkgsudo dpkg --configure -aarm上有时会遇到wayland桌面环境nomachine连上是黑底各种乱七八糟的竖条纹的情况,改回x11就会好:
sudo vim /etc/gdm3/custom.confWaylandEnable=false重启gdm3,或直接重启系统更稳妥。
5.4 关于中文环境,上面装了字体和中文编码,需要配置语言环境:
sudo dpkg-reconfigure locales选zh_CN.UTF-8 UTF-8即可,echo $LANG还不行的话:
echo "export LANGUAGE=zh_CN.zh" >> ~/.bashrcecho "export LC_ALL=zh_CN.UTF-8" >> ~/.bashrcecho "export LANG=zh_CN.UTF-8" >> ~/.bashrcecho "export LANGUAGE=zh_CN.zh" >> ~/.zshrcecho "export LC_ALL=zh_CN.UTF-8" >> ~/.zshrcecho "export LANG=zh_CN.UTF-8" >> ~/.zshrc关于输入法,就用 Fcitx5 默认,省事:
# 安装 Fcitx5 核心、中文拼音插件及图形化配置工具sudo apt updatesudo apt install fcitx5 fcitx5-chinese-addons fcitx5-frontend-gtk3 fcitx5-frontend-gtk2 fcitx5-frontend-qt5 kde-config-fcitx5 fcitx5-config-qt
# 将环境变量写入 ~/.xprofile 或 /etc/profileecho 'export GTK_IM_MODULE=fcitx' >> ~/.xprofileecho 'export QT_IM_MODULE=fcitx' >> ~/.xprofileecho 'export XMODIFIERS=@im=fcitx' >> ~/.xprofile运行im-config,选择fcitx5,重启,右上角出现 Fcitx5 的小键盘图标,右键点击选择 配置 (Configure),在右侧列表中找到 拼音 (Pinyin) 并添加到左侧。
现在使用
topgrade自动感知并更新安装工具。
5.5 关于starship
配置文件可以改好看点:
cat > ~/.config/starship.toml << 'EOF'# ==========================# 1. 机器身份识别 (防错核心)# ==========================[hostname]ssh_only = false # 即使是本地终端也显示主机名format = '在 [$hostname]($style) 'style = "bold purple" # 建议:不同机器改成不同颜色,比如测试板用红色,生产环境用紫色
# IP 显示 (仅限 SSH)[localip]ssh_only = true # 性能优化:只有在你通过 SSH 连入时才去查 IP,本地直连时不查disabled = falseformat = '🌐 [$localipv4]($style) 'style = "bold blue"
# ==========================# 2. 资源报警 (阈值触发,零延迟感知)# ==========================# 内存使用率[memory_usage]disabled = falsethreshold = 80 # 性能优化:内存占用没超过 80% 时完全不渲染此模块,节省算力format = '🐏 [$ram]($style) 'style = "bold red"
# ==========================# 3. 运维排障辅助# ==========================# 命令执行耗时 (仅当命令执行超过 2 秒时显示)[cmd_duration]min_time = 2000format = '耗时 [$duration]($style) 'style = "bold yellow"
# 退出码状态 (命令报错时变红)[status]disabled = falseformat = '[$symbol$status]($style) 'success_symbol = "🟢 "symbol = "🔴 "style = "bold red"EOF6 tailscale 与 ipv6
节后移动给的GPON坏了,运营商换了一个后发现有ipv6了。家里的rock5b、PVE工作站都可以用ipv6直连了。
PVE装tailscale
需要注意的是,PVE默认没打开ipv6支持,所以需要手动设置:
vi /etc/sysctl.conf
net.ipv6.conf.all.disable_ipv6 = 0net.ipv6.conf.default.disable_ipv6 = 0net.ipv6.conf.lo.disable_ipv6 = 0
net.ipv6.conf.all.forwarding = 1
vi /etc/sysctl.d/99-tailscale.confnet.ipv4.ip_forward = 1net.ipv6.conf.all.forwarding = 1# 或:echo 'net.ipv4.ip_forward = 1' | tee -a /etc/sysctl.d/99-tailscale.confecho 'net.ipv6.conf.all.forwarding = 1' | tee -a /etc/sysctl.d/99-tailscale.conf
# 应用网络配置ifreload -a此时用ip -6 addr show会发现vmbr0上多出来一个2409(移动)开头的地址,再用手机等公网设备打开tailscale去ping这台PVE就会从relay转到direct了。
5: vmbr0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP qlen 1000 inet6 2409:xxxx:xxx:xxxx:xxxx:xxxx:xxxx:xxxx/64 scope global dynamic mngtmpaddr proto kernel_ra valid_lft 86181sec preferred_lft 14181sec全家搬到tailscale
通过rock5b转发流量,可以把家里的192.168.1.x/24都放在tailscale上。在rock5b上配置系统转发:
echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.confecho 'net.ipv6.conf.all.forwarding = 1' | sudo tee -a /etc/sysctl.confsudo sysctl -p
sudo tailscale up --advertise-routes=192.168.1.0/24 --accept-dns=false出于安全考虑,被“广播”的网段 Tailscale 默认不信任,需要手动确认:
- 登录 Tailscale Admin Console。
- Rock 5B 点击右侧的 三个点 (…)。
- 选择 Edit route settings。
- 在弹出的窗口中,勾选广播的网段
192.168.1.0/24。 - 点击 Save。
此时,手机用运营商的网,打开tailscale,也可以使用192.168.1.0/24的IP控制家里的设备了。
6.1 tailscale可能存在的坑1 - 外带设备o6n回家后只能单向ping
6.1.1 现象
在家里的局域网 (192.168.1.0/24) 有:
- 开发板 A (o6n):
192.168.1.140 - 开发板 B (Rock5B):
192.168.1.120 - Windows 11 (含 WSL2):
192.168.1.125
症状: 开发板 A (140) 可以正常 Ping 通局域网内的 B (120) 和 Windows (125),但是 B 和 Windows 无法 Ping 通 A。呈现典型的 “单向失联” 状态。
6.1.2 排查
遇到单向不通,通常的排查链路是:物理连接 -> 防火墙 -> ARP 解析 -> 路由表。
- 排除防火墙与物理隔离: 检查了
ufw和iptables,未发现拦截规则。 - 检查 ARP 缓存 (
ip neigh show): 开发板 A 上能成功获取到 B 和 Windows 的 MAC 地址,说明物理层和数据链路层通信正常。 - 排查代理软件: 怀疑是 Windows 端的 Clash TUN 模式劫持了局域网流量。关闭 TUN 模式并使用
ping -S 192.168.1.125 192.168.1.140强制指定源 IP,依然不通,排除发件端代理问题。 - 定位核心问题——策略路由 (
ip rule): 在开发板 A (140) 上执行ip rule发现异常:执行Terminal window 5270: from all lookup 5232766: from all lookup mainip route show table 52发现:Terminal window 192.168.1.0/24 dev tailscale0
6.1.3 原因
网络中存在另一台设备——开发板 B (Rock5B, 192.168.1.120) 通过 Tailscale 执行了 --advertise-routes=192.168.1.0/24,广播了该局域网的 Subnet Route(子网路由),而开发板 A (140) 默认接受了该路由。
Tailscale 会接管系统的策略路由,创建一个高优先级(5270)的路由表 52。
当开发板 A (140) 收到局域网(比如来自 Windows 或 Rock5B 本身)发来的 Ping 请求时,它需要发送 ICMP Echo Reply(回包)。在查找路由时,它优先命中了表 52 的规则,导致本该通过物理网卡(wlan0/eth0)发回本地局域网的数据包,被错误地塞进了 Tailscale 的虚拟隧道 (tailscale0) 中,造成了“有去无回”的路由黑洞。
6.1.4 解决
由于既需要在家时局域网直连,又需要带开发板外出时能通过 Tailscale 访问家里的内网,直接关闭 accept-routes 并不符合需求。
最优解:基于源 IP 的智能分流 (Smart Routing)
在开发板 A (140) 上添加一条比 Tailscale 优先级更高(例如 2500)的策略路由规则:
sudo ip rule add from 192.168.1.140 to 192.168.1.0/24 priority 2500 lookup main原理解析:
- 在家时: 开发板 A 分配到的物理 IP 是
192.168.1.140。当局域网设备 Ping 它时,它使用该 IP 作为源 IP 回包,精确命中此规则,流量直接走main表(物理网卡),瞬间连通。 - 外出时: 开发板 A 连接其他网络,物理 IP 改变(不再是
192.168.1.140)。该规则失效,系统继续往下匹配,命中 Tailscale 的表 52,从而实现正常的异地组网访问。
避坑 尽量避免将家中的主路由网段设置为
192.168.1.0/24或192.168.0.0/24这种极其常见的默认网段。如果携带开启了异地组网的设备去酒店或单位,一旦公共 Wi-Fi 也是这个网段,就会引发严重的子网冲突(Overlapping Subnets)。建议改成类似10.88.99.0/24的冷门私有网段。
6.1.5 进阶固化:使用 systemd 实现路由规则开机自启
为了在 Armbian/Debian 12 等现代 Linux 环境下一劳永逸地解决此问题,最稳妥的方案是创建一个标准的 systemd 服务,确保在网络和 Tailscale 均就绪后,自动注入我们的高优先级本地路由规则。
Step 1: 编写路由修复脚本
创建一个专门的 Shell 脚本,采用“先清理后添加”的逻辑,防止因网络服务重启导致规则重复堆叠。
sudo vim /usr/local/bin/fix-lan-route.sh内容为:
#!/bin/bash# 忽略报错删除可能存在的旧规则,避免重复ip rule del from 192.168.1.140 to 192.168.1.0/24 priority 2500 lookup main 2>/dev/null
# 添加本地局域网高优先级规则ip rule add from 192.168.1.140 to 192.168.1.0/24 priority 2500 lookup main赋予执行权限:
sudo chmod +x /usr/local/bin/fix-lan-route.shStep 2: 编写 systemd 服务文件
配置服务依赖,确保脚本在网络连通 (network-online.target) 且 Tailscale 服务启动 (tailscaled.service) 之后再执行。
sudo vim /etc/systemd/system/fix-lan-route.service内容为:
[Unit]Description=Fix LAN routing conflict with TailscaleAfter=network-online.target tailscaled.serviceWants=network-online.target
[Service]Type=oneshotExecStart=/usr/local/bin/fix-lan-route.shRemainAfterExit=yes
[Install]WantedBy=multi-user.targetStep 3: 激活服务并验证
重载 systemd 守护进程,并将该服务设为开机自启:
# 重载系统配置sudo systemctl daemon-reload
# 设置为开机自启sudo systemctl enable fix-lan-route.service
# 立即启动该服务进行验证sudo systemctl start fix-lan-route.service最终验证:
在终端执行 ip rule 命令。如果列表中清晰地出现了 2500: from 192.168.1.140 to 192.168.1.0/24 lookup main,即代表配置大功告成。从此,无论设备重启还是随时启停 Tailscale,本地局域网的通信策略都将稳如泰山。
6.2 tailscale可能存在的坑2 - 子网发布网关rock5b开启TUN导致tailscale无法链接
6.2.1 现象:
Clash开TUN以后,tailscale启动报错。
-
开启TUN时的异常日志:
Terminal window ➜ ~ sudo systemctl status tailscaled.service● tailscaled.service - Tailscale node agentLoaded: loaded (/lib/systemd/system/tailscaled.service; enabled; vendor preset: enabled)Active: active (running) since Sat 2026-03-28 10:00:48 CST; 28s agoDocs: https://tailscale.com/docs/Main PID: 578694 (tailscaled)Tasks: 15 (limit: 18526)Memory: 54.6MCPU: 878msCGroup: /system.slice/tailscaled.service└─578694 /usr/sbin/tailscaled --state=/var/lib/tailscale/tailscaled.state --socket=/run/tailscale/tailscaled.sock --port=41641Mar 28 10:00:48 rock-5b tailscaled[578694]: logpolicy: using $STATE_DIRECTORY, "/var/lib/tailscale"Mar 28 10:00:48 rock-5b tailscaled[578694]: linkChange: in state NoState; PAC or proxyConfig changed; updating routesMar 28 10:00:48 rock-5b tailscaled[578694]: got LocalBackend in 56msMar 28 10:00:48 rock-5b tailscaled[578694]: StartMar 28 10:00:48 rock-5b tailscaled[578694]: ipnext: active extensions: conn25, portlist, posture, clientupdate, relayserver, taildropMar 28 10:00:48 rock-5b tailscaled[578694]: Backend: logs: be:de27a4d50030bb2fa9eba85172884903b5e6894feb434ee6ece1a4b0f407b618 fe:Mar 28 10:00:48 rock-5b tailscaled[578694]: control: client.Login(0)Mar 28 10:00:48 rock-5b tailscaled[578694]: health(warnable=warming-up): error: Tailscale is starting. Please wait.Mar 28 10:00:48 rock-5b tailscaled[578694]: control: doLogin(regen=false, hasUrl=false)Mar 28 10:00:53 rock-5b tailscaled[578694]: health(warnable=warming-up): ok此时:
Terminal window ➜ ~ tailscale statusunexpected state: NoState -
关闭tun后的正常日志:
Terminal window ➜ ~ sudo systemctl status tailscaled.service● tailscaled.service - Tailscale node agentLoaded: loaded (/lib/systemd/system/tailscaled.service; enabled; vendor preset: enabled)Active: active (running) since Sat 2026-03-28 10:01:28 CST; 3s agoDocs: https://tailscale.com/docs/Main PID: 579582 (tailscaled)Tasks: 15 (limit: 18526)Memory: 42.3MCPU: 708msCGroup: /system.slice/tailscaled.service└─579582 /usr/sbin/tailscaled --state=/var/lib/tailscale/tailscaled.state --socket=/run/tailscale/tailscaled.sock --port=41641Mar 28 10:01:31 rock-5b tailscaled[579582]: monitor: RTM_DELROUTE: src=198.18.0.1/0, dst=198.18.0.1/32, gw=, outif=16, table=255Mar 28 10:01:31 rock-5b tailscaled[579582]: LinkChange: major, rebinding: old: interfaces.State{defaultRoute=enP4p65s0 ifs={Meta:[198.18.0.>Mar 28 10:01:31 rock-5b tailscaled[579582]: dns: Set: {DefaultResolvers:[] Routes:{} SearchDomains:[] Hosts:6}Mar 28 10:01:31 rock-5b tailscaled[579582]: dns: Resolvercfg: {Routes:{} Hosts:6 LocalDomains:[]}Mar 28 10:01:31 rock-5b tailscaled[579582]: dns: OScfg: {}Mar 28 10:01:31 rock-5b tailscaled[579582]: wgengine: set DNS config again after major link changeMar 28 10:01:31 rock-5b tailscaled[579582]: router: portUpdate(port=41641, network=udp6)Mar 28 10:01:31 rock-5b tailscaled[579582]: Rebind; defIf="enP4p65s0", ips=[192.168.1.120/24 2409:8a70:b61:b91::1005/128 2409:8a70:b61:b91:>Mar 28 10:01:31 rock-5b tailscaled[579582]: magicsock: 0 active derp connsMar 28 10:01:31 rock-5b tailscaled[579582]: router: portUpdate(port=41641, network=udp4)此时:
Terminal window ➜ ~ tailscale status100.xx.xx.xx rock-5b xxx@ linux -100.xx.xx.xx server xxx@ linux offline, last seen xxd ago100.xx.xx.xx phone1 xxx@ android offline, last seen xxd ago100.xx.xx.xx orion-o6 xxx@ linux offline, last seen xxh ago100.xx.xx.xx phone2 xxx@ android offline, last seen xxh ago100.xx.xx.xx macbook-pro xxx@ macOS offline, last seen xxd ago
6.2.2 原因
简单来说,Clash 的 TUN 模式把 Tailscale 的建连流量给“劫持”了。
- Clash TUN 的霸道机制:当开启 Clash TUN 模式时,它会在系统里建一个虚拟网卡(比如
Mihomo),并通过修改系统路由表(下发0.0.0.0/1这种默认路由)或者用fwmark(防火墙标记)把机器上所有的流量都强行吸进自己的核心里。同时,它通常还会开启 Fake-IP 来劫持 DNS 请求(日志里关闭 TUN 时出现的198.18.0.1就是 Clash 的 Fake-IP 网段)。 - Tailscale 的迷失:
tailscaled服务启动时,需要向它自己的控制面板(login.tailscale.com)和分布在全球的 DERP 中继服务器发送 UDP 握手包来建立状态。 - 死锁形成:开启 TUN 后,Tailscale 刚发出建连请求,就被 Clash 一把抓走。如果这些流量被 Clash 丢到了错误的代理节点,或者由于 Fake-IP 导致 Tailscale 拿不到中继服务器的真实 IP,它的握手包就永远发不出去,一直卡在
NoState等待响应。
6.2.3 解决
思路很简单:给 Clash 设个规矩,让它对 Tailscale 相关的 IP、域名和进程“高抬贵手”,直接放行。
添加 Merge (合并) 路由规则
为了万无一失,我们需要让 Mihomo 核心彻底放行 tailscaled 进程和相关域名的流量。
将以下规则添加到直连规则(Direct)中,并且要放在规则列表的 最前面:
rules: # 直连 Tailscale 后台守护进程 - PROCESS-NAME,tailscaled,DIRECT # 直连 Tailscale 的内网网段 - IP-CIDR,100.64.0.0/10,DIRECT # 直连 Tailscale 的控制面域名 - DOMAIN-SUFFIX,tailscale.com,DIRECT - DOMAIN-SUFFIX,tailscale.io,DIRECT修复 Fake-IP 污染(关键):如果 Clash 配置里开启了 Fake-IP,必须把 Tailscale 的域名加入 fake-ip-filter,否则它连不上中继。在配置(实际配置位于dns_config.yaml)中补充:
dns: fake-ip-filter: - '*.tailscale.com' - '*.tailscale.io'应用这些设置并重启 Clash 的 TUN 开关后,Clash 就会把 Tailscale 的建连流量和 100.x.x.x 的内网流量原封不动地交还给系统路由表,两边就能和平共处了。
注:关于如何查找配置文件位置
在 Linux 中,要查看一个进程在启动时到底读取了哪些文件,最强大且标准的工具是 strace。它可以追踪进程调用的所有系统 API,包括打开文件的操作。
因为 clash-verge 是基于 Tauri 的应用,它会启动多个子进程(前端渲染、后端服务等),所以需要加上特定参数来追踪所有子进程,并过滤出跟配置文件相关的操作。
Step1: 进程追踪
安装 strace ,用 strace 运行组合命令并输出日志,将 strace 加在 clash 环境变量之后、 clash-verge 之前。为了防止终端输出被海量的系统调用淹没,把日志重定向到一个文本文件中:
sudo apt install strace
WEBKIT_DISABLE_COMPOSITING_MODE=1 WEBKIT_DISABLE_DMABUF_RENDERER=1 WEBKIT_DISABLE_SANDBOX_THIS_IS_DANGEROUS=1 GDK_BACKEND=x11 strace -f -e trace=open,openat clash-verge 2> startup_trace.log参数解释:
-f:告诉strace追踪所有由clash-verge派生出来的子进程(关键,因为配置通常是核心子进程读取的)。-e trace=open,openat:只监听“打开文件”相关的系统调用,过滤掉网络、内存等无关操作。2> startup_trace.log:将strace的标准错误输出(它的追踪日志默认输出到这里)保存到startup_trace.log文件中。
运行上面这条命令后,等终端停留在之前的状态(说明应用已经启动并转入后台监听),按 Ctrl + C 结束它。
Step2: 分析追踪日志
现在当前目录下生成了一个 startup_trace.log 文件。可以用 grep 过滤出所有常见的配置文件后缀(如 .yaml, .yml, .json):
grep -i -E '\.yaml|\.yml|\.json' startup_trace.log | grep -v "ENOENT"参数解释:
-i -E '\.yaml|\.yml|\.json':忽略大小写,匹配这三种常见的配置文件格式。grep -v "ENOENT":过滤掉“文件不存在(Error No Entry)”的记录。程序在启动时通常会按照一定顺序去多个默认目录“试探性”地寻找配置文件,过滤掉找不到的记录,剩下的就是它真正成功读取到的文件路径:➜ ~ grep -i -E '\.yaml|\.yml|\.json' startup_trace.log | grep -v "ENOENT"[pid 568056] openat(AT_FDCWD, "/home/pi/.local/share/io.github.clash-verge-rev.clash-verge-rev/verge.yaml", O_RDONLY|O_CLOEXEC) = 19[pid 568057] openat(AT_FDCWD, "/home/pi/.local/share/io.github.clash-verge-rev.clash-verge-rev/config.yaml", O_RDONLY|O_CLOEXEC) = 19[pid 568057] openat(AT_FDCWD, "/home/pi/.local/share/io.github.clash-verge-rev.clash-verge-rev/verge.yaml", O_RDONLY|O_CLOEXEC) = 19[pid 568056] openat(AT_FDCWD, "/home/pi/.local/share/io.github.clash-verge-rev.clash-verge-rev/profiles.yaml", O_RDONLY|O_CLOEXEC) = 19[pid 568036] openat(AT_FDCWD, "/home/pi/.config/io.github.clash-verge-rev.clash-verge-rev/window_state.json", O_RDONLY|O_CLOEXEC) = 20[pid 568057] openat(AT_FDCWD, "/home/pi/.local/share/io.github.clash-verge-rev.clash-verge-rev/profiles/RrCXXb9eVReP.yaml", O_RDONLY|O_CLOEXEC) = 30[pid 568058] openat(AT_FDCWD, "/home/pi/.local/share/io.github.clash-verge-rev.clash-verge-rev/profiles/Merge.yaml", O_RDONLY|O_CLOEXEC <unfinished ...>[pid 568058] openat(AT_FDCWD, "/home/pi/.local/share/io.github.clash-verge-rev.clash-verge-rev/dns_config.yaml", O_RDONLY|O_CLOEXEC) = 21[pid 568057] openat(AT_FDCWD, "/home/pi/.local/share/io.github.clash-verge-rev.clash-verge-rev/clash-verge.yaml", O_WRONLY|O_CREAT|O_TRUNC|O_CLOEXEC, 0666) = 21[pid 568058] openat(AT_FDCWD, "/home/pi/.local/share/io.github.clash-verge-rev.clash-verge-rev/clash-verge-check.yaml", O_WRONLY|O_CREAT|O_TRUNC|O_CLOEXEC, 0666) = 21[pid 568095] openat(AT_FDCWD, "/home/pi/.local/share/io.github.clash-verge-rev.clash-verge-rev/clash-verge-check.yaml", O_RDONLY|O_CLOEXEC) = 3[pid 568057] openat(AT_FDCWD, "/home/pi/.local/share/io.github.clash-verge-rev.clash-verge-rev/clash-verge.yaml", O_WRONLY|O_CREAT|O_TRUNC|O_CLOEXEC, 0666) = 21```
7 self host atuin
atuin 的主机间 sync 命令行历史记录非常有用,按照官方说明 Self Hosting - Docker Compose 用 Docker Compose 的方式可以很容易的拉起来一个私有服务。之后,在任意需要 atuin sync 的主机上,修改 atuin 的配置文件 ~/.config/atuin/config.toml:sync_address = "http://<server-ip:port>"即可。
将本地的shell命令导入 atuin 管理,运行atuin import auto或atuin import zsh。注:如果要 atuin 接管 ctrl-r,则要保证.zshrc中 atuin 在 fzf 之后初始化。
接下来,在第一台要同步的主机命令行里,运行首次用户注册:atuin register -u <username> -e <email>,此时 atuin 会让你运行atuin key拿到助记词,用来在之后的登录时使用。注册完毕后运行atuin sync向服务器同步历史记录。完成注册后,再在其他主机上只需要执行登录即可:atuin login -u <username>再输入助记词即可,成功后atuin sync。
此时ctrl-r打开 atuin ,多次按ctrl-r就会在 全局、本机、本次会话、此目录 等环境中切换。在全局环境,选中命令按ctrl-o进入检视页会看到此命令所属的主机。
最后设置系统服务,新建sudo vim /etc/systemd/system/atuin.service,把官方说明中systemd那段脚本贴进去,然后将WorkingDirectory换成刚才docker-compose.yml所在的绝对路径即可,再sudo systemctl daemon-reload、sudo systemctl enable --now atuin.service、sudo systemctl status atuin.service即可看到开机自启的 atuin 服务。
8 启用 rock 5b 的 pwm
风扇的pwm线插在pin 27上,sudo armbian-config进入TUI界面 -> System -> Kernel -> DTO01 - Manage device tree overlays -> 选 rk3588-pwm7-m3。或者在/boot/armbianEnv.txt里overlays后面续上字符串rk3588-pwm7-m3。
重启。
PWM 子系统的 pwmchipN 编号是按 probe 顺序生成的,每次启动可能不一样,必须看一下:
sudo cat /sys/kernel/debug/pwmplatform/febd0030.pwm, 1 PWM device pwm-0 ((null) ): period: 0 ns duty: 0 ns polarity: normal
platform/fd8b0010.pwm, 1 PWM device pwm-0 (pwm-fan ): requested period: 60000 ns duty: 0 ns polarity: normalfebd0030 这个地址就是 PWM7(RK3588 PWM7 寄存器基址)。然后看哪个 pwmchip 链接到它:
ls -l /sys/class/pwm/lrwxrwxrwx - root 29 4月 18:51 pwmchip0 -> ../../devices/platform/fd8b0010.pwm/pwm/pwmchip0lrwxrwxrwx - root 29 4月 18:51 pwmchip1 -> ../../devices/platform/febd0030.pwm/pwm/pwmchip1记住这个pwmchip1
先写个脚本测试:
uv init pwm7-m3cd pwm7-m3uv add python-periphery写一个python脚本 fan_pwm.py 测试:
"""Hardware PWM fan control on Rock 5B PIN_27 (GPIO4_C6 / PWM7_IR_M3).
需先启用 rk3588-pwm7-m3 overlay 并重启。通过 /sys/kernel/debug/pwm 找到 febd0030.pwm 对应的 pwmchip 号。"""
import signalimport sysimport time
from periphery import PWM
# === 改成你机器上实际看到的 pwmchip 号 ===PWM_CHIP = 1 # pwmchipN 中的 NPWM_CHANNEL = 0 # Rockchip 每个 chip 只有一个 channel,固定为 0# =====================================
FREQ_HZ = 25000 # 4 线 PC 风扇标准 PWM 频率DUTY = 0.30 # 30% 占空比
def main() -> None: pwm = PWM(PWM_CHIP, PWM_CHANNEL) try: pwm.frequency = FREQ_HZ # 25 kHz pwm.duty_cycle = DUTY # 0.0 ~ 1.0 pwm.polarity = "normal" pwm.enable()
print(f"PWM7 on PIN_27: {DUTY * 100:.0f}% @ {FREQ_HZ} Hz " f"(pwmchip{PWM_CHIP})") print("Ctrl+C 退出")
def shutdown(_sig, _frame): print("\n停止 PWM...") pwm.disable() pwm.close() sys.exit(0)
signal.signal(signal.SIGINT, shutdown) signal.signal(signal.SIGTERM, shutdown)
while True: time.sleep(1) except Exception: pwm.close() raise
if __name__ == "__main__": main()运行脚本测试 sudo $(which uv) run python fan_pwm.py,此时风扇应该降速了。
成功后封装为系统服务,服务运行时不能依赖家目录(特别是如果以 root 跑),放到 /opt:
sudo mv ~/pwm7-m3 /opt/pwm7-m3sudo chown -R root:root /opt/pwm7-m3cd /opt/pwm7-m3确认依赖装好(uv 会在 /opt/pwm7-m3/.venv 建虚拟环境):
sudo $(which uv) sync记下 uv 的绝对路径,service 里要用:
which uv/home/radxa/.local/bin/uv由于 uv 装在家目录下(~/.local/bin/uv),root 跑服务时找不到,把 uv 放到系统路径:
sudo cp ~/.local/bin/uv /usr/local/bin/uv微调一下脚本,让它对 SIGTERM(systemd stop 发的信号)也能干净退出,并且把输出冲到日志里(journald 会自动收):
"""Hardware PWM fan control on Rock 5B PIN_27 (PWM7_IR_M3)."""
import signalimport sysimport time
from periphery import PWM
PWM_CHIP = 1 # 改成你机器上 febd0030.pwm 对应的 pwmchipNPWM_CHANNEL = 0FREQ_HZ = 25000DUTY = 0.30
def main() -> None: pwm = PWM(PWM_CHIP, PWM_CHANNEL) pwm.frequency = FREQ_HZ pwm.duty_cycle = DUTY pwm.polarity = "normal" pwm.enable()
print(f"pwm7-m3: {DUTY * 100:.0f}% @ {FREQ_HZ} Hz on pwmchip{PWM_CHIP}", flush=True)
stopping = False
def shutdown(_sig, _frame): nonlocal stopping stopping = True
signal.signal(signal.SIGINT, shutdown) signal.signal(signal.SIGTERM, shutdown)
try: while not stopping: time.sleep(1) finally: print("pwm7-m3: stopping", flush=True) try: pwm.disable() finally: pwm.close()
if __name__ == "__main__": sys.exit(main())flush=True 是为了让 print 立刻被 systemd 抓到日志里,不然 Python 默认会做行缓冲/块缓冲,停服务前可能看不到输出。
创建 systemd service 文件:
sudo vim /etc/systemd/system/pwm7-m3.service
[Unit]Description=Rock 5B PWM fan control (PIN_27 / PWM7)After=multi-user.target
[Service]Type=simpleWorkingDirectory=/opt/fan-pwmExecStart=/usr/local/bin/uv run python fan_pwm.pyRestart=on-failureRestartSec=3User=rootStandardOutput=journalStandardError=journal
[Install]WantedBy=multi-user.target几个要点:
Type=simple:脚本本身就是常驻进程,不 fork,所以 simple 即可。ExecStart里 uv 必须用绝对路径——systemd 不读的 shellPATH。WorkingDirectory让 uv 找得到项目里的pyproject.toml和.venv。Restart=on-failure:脚本异常退出就重启,但systemctl stop主动停的时候不会重启。User=root:因为 PWM sysfs 默认只有 root 能写。
启用 + 启动,enable --now 等于 enable(开机自启)+ start(立刻启动)一起做。
sudo systemctl daemon-reloadsudo systemctl enable --now pwm7-m3.service常用操作:
# 看状态sudo systemctl status pwm7-m3
# 看实时日志sudo journalctl -u pwm7-m3 -f
# 停止 / 重启sudo systemctl stop pwm7-m3sudo systemctl restart pwm7-m3
# 改完占空比想生效,重启服务即可sudo systemctl restart pwm7-m3
# 不要开机自启了sudo systemctl disable pwm7-m3成功后也可以把脚本改成自动控温的模式:
"""Temperature-based PWM fan control for Rock 5B (PIN_27 / PWM7_IR_M3)."""
import signalimport sysimport time
from periphery import PWM
# === 硬件配置 ===PWM_CHIP = 1 # febd0030.pwm 对应的 pwmchipNPWM_CHANNEL = 0FREQ_HZ = 25000THERMAL_ZONE = "/sys/class/thermal/thermal_zone0/temp" # CPU 温度POLL_INTERVAL = 3.0 # 秒,温度采样间隔
# === 风扇曲线 ===# (温度 °C, 占空比 0.0~1.0)# 大风扇没必要跑全速,最高也就 60%FAN_CURVE = [ (45.0, 0.30), # 45°C 以下30% (50.0, 0.35), # 起转点 (60.0, 0.40), (70.0, 0.45), (80.0, 0.60), # 80°C 以上也只到 60%]
# 滞回,避免在阈值附近反复启停HYSTERESIS_C = 2.0
# 起转辅助:从 0% 跳到目标值时,先全力踹一下让风扇转起来KICKSTART_DUTY = 0.50KICKSTART_SEC = 1.0
def read_temp_c() -> float: with open(THERMAL_ZONE) as f: return int(f.read().strip()) / 1000.0
def duty_for_temp(temp_c: float, curve: list[tuple[float, float]]) -> float: """在曲线上做线性插值。""" if temp_c <= curve[0][0]: return curve[0][1] if temp_c >= curve[-1][0]: return curve[-1][1] for (t0, d0), (t1, d1) in zip(curve, curve[1:]): if t0 <= temp_c <= t1: ratio = (temp_c - t0) / (t1 - t0) return d0 + (d1 - d0) * ratio return curve[-1][1]
def main() -> int: pwm = PWM(PWM_CHIP, PWM_CHANNEL) pwm.frequency = FREQ_HZ pwm.duty_cycle = 0.0 pwm.polarity = "normal" pwm.enable()
print(f"fan-pwm: started, curve={FAN_CURVE}", flush=True)
stopping = False
def shutdown(_sig, _frame): nonlocal stopping stopping = True
signal.signal(signal.SIGINT, shutdown) signal.signal(signal.SIGTERM, shutdown)
last_temp = read_temp_c() current_duty = 0.0
try: while not stopping: temp = read_temp_c()
# 滞回:温度变化小于 HYSTERESIS_C 时不动 if abs(temp - last_temp) < HYSTERESIS_C: target = current_duty else: target = duty_for_temp(temp, FAN_CURVE) last_temp = temp
if target != current_duty: # 从停转到要起转 → 先 kickstart if current_duty == 0.0 and target > 0.0: pwm.duty_cycle = max(target, KICKSTART_DUTY) print(f"fan-pwm: {temp:.1f}°C kickstart " f"-> {pwm.duty_cycle * 100:.0f}%", flush=True) time.sleep(KICKSTART_SEC)
pwm.duty_cycle = target current_duty = target print(f"fan-pwm: {temp:.1f}°C duty -> {target * 100:.0f}%", flush=True)
time.sleep(POLL_INTERVAL) finally: print("fan-pwm: stopping", flush=True) try: pwm.duty_cycle = 0.0 pwm.disable() finally: pwm.close() return 0
if __name__ == "__main__": sys.exit(main())