MoreRSS

site iconLiHan | 李寒修改

研究生,中国传媒大学
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

LiHan | 李寒的 RSS 预览

APlayer 歌词不显示?先把 LRC 时间戳规范化

2026-06-04 18:43:00

Featured image of post APlayer 歌词不显示?先把 LRC 时间戳规范化

一次性的小坑记录,面向人阅读。结论先放这:APlayer 歌词某些行不显示,十有八九是时间戳格式不对,不是歌词内容缺了。

症状

给博客的 APlayer 播放器挂歌词(.lrc)后,有些歌一切正常,有些歌却有几行死活不显示 / 不滚动——可歌词文件里那几行明明在。控制台也不报错,就是静悄悄地少了几句。

根因

APlayer 的 LRC 解析器对时间戳挑食:它只稳定接受 [mm:ss.xx][mm:ss.xxx](两位或三位毫秒)这种"规整"写法。遇到下面这些松散写法,它会直接跳过整行

  • [02:54.4] —— 只有一位小数
  • [2:54.40] —— 分钟只有一位
  • [02:54] 之外各种位数不齐的组合

而很多歌词来源(网易云导出、第三方站点、手抄)恰恰就是这种松散格式。于是同一份歌单里,规整的歌好好的,松散的歌就缺行——表现得很"随机",其实规律就是时间戳。

修复:发布前规范化时间戳

思路很简单:把每个时间戳重算成总秒数,再补零、把毫秒补齐到两/三位。核心就一段正则替换:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import re
from decimal import Decimal, ROUND_HALF_UP

LRC_TS = re.compile(r"\[(\d{1,2}):(\d{1,2})(?:\.(\d+))?\]")

def _normalize(m: re.Match) -> str:
 mm, ss, frac = m.group(1), m.group(2), m.group(3)
 total = int(mm) * 60 + int(ss)
 if frac is None:
 return f"[{total // 60:02d}:{total % 60:02d}]"
 if len(frac) in (2, 3): # 已规整
 return f"[{total // 60:02d}:{total % 60:02d}.{frac}]"
 if len(frac) == 1: # [02:54.4] -> .400
 ms = int(frac) * 100
 else: # 四位以上,四舍五入到毫秒
 ms = int((Decimal(f"0.{frac}") * 1000).quantize(Decimal("1"), ROUND_HALF_UP))
 if ms >= 1000:
 total, ms = total + 1, 0
 return f"[{total // 60:02d}:{total % 60:02d}.{ms:03d}]"

def normalize_lrc(text: str) -> str:
 return "\n".join(LRC_TS.sub(_normalize, line) for line in text.splitlines())

[02:54.4][02:54.400][2:5][02:05],APlayer 就都认了。

现在:已经自动化,不用再手动改

这一步已经接进 lihan3238/music 仓库的 CI——lrc_compat.py 里的 normalize_lrc_filetest.py 调用,music-sync.ymllrc/** 变更时自动跑,发布前自动规范化。所以现在往歌单里丢任意来源的 .lrc,流水线会自己把时间戳修好,前端再也不会缺行。

一句话

APlayer 歌词不显示,先查时间戳是不是 [mm:ss.xx] / [mm:ss.xxx],别先怀疑歌词内容或播放器本身。

Hugo Stack v4 迁移地图

2026-06-03 00:00:00

Featured image of post Hugo Stack v4 迁移地图

本文是一次真实迁移的完整走查记录(一次性、面向人阅读)。可被 AI 直接调用、 带可复用 verify 的精简版,见知识卡片 hugo-stack-v4-migration-map/ai/cards/hugo-stack-v4-migration-map.md)。

背景

把本博客从 Stack v3 升级到 v4。验证于 2026-06-03 UTC(北京时间 2026-06-04), 环境为 WSL。适用场景:升级本博客或其他基于 Stack 主题的 Hugo 站点到 v4,尤其 当出现 CI 卡住、自定义 partial 消失、link-card 页面在构建期抓取远程图片、或旧 frontmatter 不再影响页面可见性时。

迁移地图

  • Hugo 版本下限:Stack v4 要求 Hugo >=0.157.0;本博客 CI 使用 0.162.1
  • 模块路径:导入 github.com/CaiJimmy/hugo-theme-stack/v4,并在 go.mod 中 require github.com/CaiJimmy/hugo-theme-stack/v4 v4.x
  • Partial 覆盖:Stack v4 使用 layouts/_partials/**,而非 layouts/partials/**。把自定义 partial 移到那里。
  • 语言键:简体中文是 zh,不是 zh-cn;使用 locale = "zh"defaultContentLanguage = "zh"
  • Favicon:设置 favicon = "favicon.ico" 并把文件放到 assets/ 下; 仅为了向后兼容旧 URL 才保留 static/favicon.ico
  • 侧栏头像:v4 期望 [sidebar] avatar = "img/avatar.jpg",而非 v3 的 [sidebar.avatar] enabled/local/src 对象。
  • 图片处理:v4 把 imageProcessing.cover 改名为 imageProcessing.thumbnail;内容图片处理仍保留 [imageProcessing.content]
  • 隐藏页面:v4 移除了 Stack 的 hidden frontmatter 行为。对必须不出现在 列表中的页面,改用 Hugo 构建选项,例如 build.list = "never"
  • 菜单图标:使用菜单 params.icon;不要再依赖 .Pre
  • OpenGraph 默认图:v4 去掉了旧的默认回退配置;移除 [defaultImage.opengraph]
  • 排序:v4 支持 SortBy = "lastmod"。为了 CI 行为稳定,配合 enableGitInfo = true、完整的 checkout 历史,以及 [frontmatter].lastmod = ["lastmod", "modified", ":git", "date", "publishDate"]
  • Link-card 图片:v4 原生 links 组件会在 Hugo 构建期抓取远程 link 图片。 对本博客,覆盖 article/components/links.html,直接渲染 <img src="...">, 让浏览器加载 favicon,CI 不依赖远程站点。

可用 v4 原生能力替换

  • Markdown alerts 通过 [article.alertIcon] 原生支持;普通 note/tip/warning 块不再需要自定义 admonition shortcode。
  • Mermaid 代码块通过 [article.mermaid] 原生支持;图表不再需要自定义 Mermaid shortcode 或页面级加载器。
  • 文章内容中的外部图片现在参与 Photoswipe v5 画廊/灯箱。
  • 当默认布局够用时,通用 taxonomy 组件可替换定制的 category/tag 组件。
  • 当目标是“最近编辑”排序时,SortBy = "lastmod" 可替换本地的列表 hack。
  • Comentario 与 GDPR cookie consent 是原生选项,但仅在站点有相应隐私/评论 政策时才启用。

需要保留的自定义

  • APlayer 音乐播放器,包括自动列表注入与固定播放 UX。
  • 自定义搜索 query-param 行为。
  • Mastodon 想法时间线。
  • 分享按钮。
  • 密码保护的公开文章包装。
  • CI 友好的、构建期稳定的 link-card 图片渲染。

迁移清单

  1. 在逐个修报错之前,先读官方 v4 升级指南与本地 v4 默认配置。
  2. 更新 Hugo、模块路径、go.modgo.sum、语言键、favicon、头像、图片处理, 以及被移除的配置块。
  3. 把自定义 partial 从 layouts/partials/** 移到 layouts/_partials/**; 同步更新任何 workflow 路径(例如 music sync)。
  4. 扫描 content/frontmatter 中的 hidden: trueimageProcessing.coverlayouts/partials/.Pre,以及被误写成 Markdown 图片的普通链接。
  5. 用接近空的缓存、以 CI 的 Hugo 版本构建;远程抓取警告通常意味着某个非图片 URL 被当作图片,或 links partial 在构建期做了远程工作。
  6. 用 Playwright MCP 走查首页、搜索、关键自定义页面、link-card 页面、一篇文章 页面,以及一个移动端视口。

验证

1
2
HUGO_ENVIRONMENT=production hugo --minify --baseURL https://lihan3238.github.io/ \
 && actionlint && git diff --check

期望:Hugo >=0.157 无警告构建;workflow 通过 lint;diff 无空白错误;合并前 PR 的 Pages 构建通过。若失败,检查模块路径、partial 覆盖位置、 avatar/favicon/imageProcessing 键、隐藏 frontmatter,以及构建期远程资源抓取。

基于 OpenWrt 和 WireGuard 的远程组网与运维方案

2026-06-02 20:00:00

Featured image of post 基于 OpenWrt 和 WireGuard 的远程组网与运维方案

基于 OpenWrt 和 WireGuard 的远程组网与运维方案

1. 目标

用一台有公网入口的 Rocky Linux VPS 做 WireGuard Hub,把宿舍 OpenWrt、工位 Linux、工位 Windows 等设备接入同一个私有网段,并通过宿舍 OpenWrt 远程唤醒宿舍 Windows PC。

本文不是 WireGuard 原理课,而是一份部署和维护手册。以后忘了命令、要新增 peer、要排查握手和路由问题时,优先按本文检查。

当前策略:

  • VPS 只做 WireGuard Hub 和转发中心。
  • 宿舍 OpenWrt 接入 WireGuard,并代表宿舍 LAN 192.168.1.0/24
  • 工位 Linux/Windows 作为单设备 peer 接入,只宣告自己的 /32 地址。
  • 不再尝试在 RT-AC88U 386.14_2 上强行部署 WireGuard 主链路。
  • 旧 OpenVPN 可暂时保留,继续管理旧网段,等 WireGuard 稳定后再逐步替换。

注意:本文是公开博客版本,所有私钥、公钥、MAC 地址、VPS 公网 IP 都用占位符表示。真实值只应保存在对应设备本机配置中,不要放进 GitHub、截图或聊天记录。


2. 当前拓扑

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
VPS / Rocky Linux
 wg0: 10.77.0.1/24
 UDP: 51820
 PublicKey: VPS_PUBLIC_KEY

宿舍 OpenWrt
 wg0: 10.77.0.2/32
 LAN: 192.168.1.0/24
 LAN gateway: 192.168.1.1
 LAN device: br-lan
 PublicKey: DORM_OPENWRT_PUBLIC_KEY

工位 Linux / ThinkStation
 wg0: 10.77.0.101/32
 PublicKey: OFFICE_LINUX_PUBLIC_KEY

工位 Windows
 建议地址: 10.77.0.102/32
 类型: 单设备 peer,不宣告整个 192.168.50.0/24

旧 OpenVPN
 10.88.0.0/24
 192.168.50.0/24 可暂时继续由 OpenVPN 管理

推荐访问方式:

1
2
3
4
5
6
7
8
访问 VPS: 10.77.0.1
访问宿舍 OpenWrt: 10.77.0.2 或 192.168.1.1
访问工位 Linux: 10.77.0.101
访问工位 Windows: 10.77.0.102
访问宿舍 PC: 192.168.1.x

远程唤醒宿舍 PC:
 ssh [email protected] /root/wake-dorm-pc.sh

3. 地址规划与设计原则

3.1 网段规划

当前网段:

1
2
3
4
WireGuard 主网段: 10.77.0.0/24
旧 OpenVPN 网段: 10.88.0.0/24
宿舍 LAN: 192.168.1.0/24
工位 LAN: 192.168.50.0/24

后续如果有条件统一调整 LAN,建议改成不容易冲突的编号:

1
2
宿舍 LAN: 192.168.201.0/24
工位 LAN: 192.168.202.0/24

但当前阶段不强行修改 LAN。远程组网最怕“边调边失联”,先把 WireGuard 跑稳,再做地址治理。

3.2 Hub-and-Spoke,而不是全互联

本方案采用:

1
所有 peer -> VPS Hub -> 其他 peer / 宿舍 LAN

理由:

  • VPS 有公网入口,最适合做固定 Endpoint。
  • 宿舍 OpenWrt 通常在 NAT 后面,主动连 VPS 更稳定。
  • 工位 Linux/Windows 作为单机 peer 管理更清晰,不把工位 LAN 路由责任塞给普通终端。
  • 新增 peer 只需要改 VPS 和新设备,不需要每台设备互相加配置。

3.3 AllowedIPs 的核心规则

WireGuard 的 AllowedIPs 同时承担两件事:

1
2
1. 这个 peer 允许使用哪些源 IP
2. 哪些目标 IP 应该发给这个 peer

因此它既像访问控制列表,又像路由表。配置错误时,经常表现为“有 handshake,但 ping 不通”。

规则:

  • VPS 上:哪个 peer 真正负责某个网段,哪个 peer 才能宣告这个网段。
  • 宿舍 OpenWrt:可以宣告 10.77.0.2/32192.168.1.0/24
  • 工位 Linux/Windows:只宣告自己的 10.77.0.x/32
  • 工位 Linux/Windows 不宣告 192.168.50.0/24,除非它真的是工位网关。
  • OpenWrt 客户端侧的 AllowedIPs 写“远端目标”,不要把自己的本地 LAN 写进去。

错误示例:

1
2
3
4
5
6
7
[Peer]
# dorm-openwrt
AllowedIPs = 10.77.0.2/32, 192.168.1.0/24

[Peer]
# dorm-laptop
AllowedIPs = 10.77.0.30/32, 192.168.1.0/24

这里两个 peer 同时宣告 192.168.1.0/24,会造成路由冲突。

正确示例:

1
2
3
4
5
6
7
[Peer]
# dorm-openwrt
AllowedIPs = 10.77.0.2/32, 192.168.1.0/24

[Peer]
# dorm-laptop
AllowedIPs = 10.77.0.30/32

4. VPS / Rocky Linux 服务端

4.1 安装与检查

1
2
3
4
5
sudo dnf install -y wireguard-tools

sudo modprobe wireguard
lsmod | grep wireguard
wg --version

正常应看到:

1
2
wireguard ...
wireguard-tools v...

4.2 生成服务端密钥

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
sudo install -d -m 700 /etc/wireguard

sudo bash -c '
set -e
umask 077

if [ ! -f /etc/wireguard/server_private.key ]; then
 wg genkey > /etc/wireguard/server_private.key
fi

wg pubkey < /etc/wireguard/server_private.key > /etc/wireguard/server_public.key

chmod 600 /etc/wireguard/server_private.key
chmod 644 /etc/wireguard/server_public.key
'

查看公钥:

1
sudo cat /etc/wireguard/server_public.key

注意:

1
2
3
server_private.key 只留在 VPS。
server_public.key 复制给各客户端。
如果私钥贴到聊天、文档或 GitHub,直接重新生成该 peer 的密钥。

4.3 VPS 配置 /etc/wireguard/wg0.conf

1
sudo nano /etc/wireguard/wg0.conf

推荐模板:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
[Interface]
# Rocky VPS WireGuard Hub
Address = 10.77.0.1/24
ListenPort = 51820
PrivateKey = VPS_PRIVATE_KEY
SaveConfig = false

[Peer]
# dorm-openwrt
PublicKey = DORM_OPENWRT_PUBLIC_KEY
AllowedIPs = 10.77.0.2/32, 192.168.1.0/24

[Peer]
# office-linux-thinkstation
PublicKey = OFFICE_LINUX_PUBLIC_KEY
AllowedIPs = 10.77.0.101/32

# [Peer]
# # office-windows
# PublicKey = OFFICE_WINDOWS_PUBLIC_KEY
# AllowedIPs = 10.77.0.102/32

建议固定:

1
SaveConfig = false

原因是配置文件才是真源。否则运行时的临时变更可能被 wg-quick 写回文件,长期维护时容易混乱。

4.4 开启 IPv4 转发

1
2
3
4
echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/99-wireguard.conf
sudo sysctl --system

sysctl net.ipv4.ip_forward

期望:

1
net.ipv4.ip_forward = 1

4.5 配置 firewalld

放行 WireGuard 端口:

1
2
3
4
sudo firewall-cmd --add-port=51820/udp --permanent
sudo firewall-cmd --reload

sudo firewall-cmd --list-ports

建议把 wg0 放入 trusted zone,减少 VPN 内部转发被 firewalld 拦截的概率:

1
2
3
4
sudo firewall-cmd --permanent --zone=trusted --add-interface=wg0
sudo firewall-cmd --reload

sudo firewall-cmd --get-active-zones

如果 VPS 还有云厂商安全组,也要放行:

1
入站 UDP 51820

4.6 启动与维护

启动:

1
sudo systemctl enable --now wg-quick@wg0

重启:

1
sudo systemctl restart wg-quick@wg0

停止:

1
sudo systemctl stop wg-quick@wg0

查看状态:

1
2
3
sudo systemctl status wg-quick@wg0 --no-pager
sudo wg show
ip addr show wg0

查看关键路由:

1
ip route | grep -E '10\.77|10\.88|192\.168\.1|192\.168\.50|default'

正常状态应至少包含:

1
2
3
default via ... dev ens...
10.77.0.0/24 dev wg0 proto kernel scope link src 10.77.0.1
192.168.1.0/24 dev wg0

如果旧 OpenVPN 仍在,可能还会看到:

1
2
10.88.0.0/24 via ... dev tun0
192.168.50.0/24 via ... dev tun0

5. 宿舍 OpenWrt 配置

5.1 当前接口确认

宿舍 OpenWrt 当前 LAN:

1
2
3
4
5
6
7
network.lan.device='br-lan'
network.lan.ipaddr='192.168.1.1'
network.lan.netmask='255.255.255.0'

br-lan: 192.168.1.1/24
wg0: 10.77.0.2/32
wan: pppoe-wan

后续 WOL 必须从 br-lan 发,不要从 wg0pppoe-wan 发。

5.2 安装 WireGuard

1
2
3
4
opkg update
opkg install wireguard-tools kmod-wireguard luci-proto-wireguard

wg --version

5.3 生成 OpenWrt 密钥

1
2
3
4
5
6
umask 077
wg genkey > /etc/wireguard_openwrt_private.key
wg pubkey < /etc/wireguard_openwrt_private.key > /etc/wireguard_openwrt_public.key

chmod 600 /etc/wireguard_openwrt_private.key
cat /etc/wireguard_openwrt_public.key

将输出的 DORM_OPENWRT_PUBLIC_KEY 填到 VPS 的 dorm-openwrt peer 中。

5.4 用 UCI 配置 wg0

先设置变量:

1
2
3
4
OPENWRT_PRIV="$(cat /etc/wireguard_openwrt_private.key)"
VPS_PUB="VPS_PUBLIC_KEY"
VPS_HOST="VPS_PUBLIC_IP_OR_DOMAIN"
VPS_PORT="51820"

配置接口和 peer:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
uci -q delete network.wg0
uci -q delete network.wg0_peer_vps

uci set network.wg0='interface'
uci set network.wg0.proto='wireguard'
uci set network.wg0.private_key="$OPENWRT_PRIV"
uci add_list network.wg0.addresses='10.77.0.2/32'

uci set network.wg0_peer_vps='wireguard_wg0'
uci set network.wg0_peer_vps.description='rocky-vps'
uci set network.wg0_peer_vps.public_key="$VPS_PUB"
uci set network.wg0_peer_vps.endpoint_host="$VPS_HOST"
uci set network.wg0_peer_vps.endpoint_port="$VPS_PORT"
uci set network.wg0_peer_vps.persistent_keepalive='25'
uci set network.wg0_peer_vps.route_allowed_ips='1'
uci add_list network.wg0_peer_vps.allowed_ips='10.77.0.0/24'

uci commit network
/etc/init.d/network reload

关键点:

1
2
3
OpenWrt 客户端侧 AllowedIPs 写远端目标。
宿舍本地 LAN 192.168.1.0/24 不写在 OpenWrt 对 VPS 的 AllowedIPs 里。
VPS 端才写:AllowedIPs = 10.77.0.2/32, 192.168.1.0/24

5.5 修改 OpenWrt AllowedIPs

查看:

1
uci show network.wg0_peer_vps

只保留 WireGuard 网段:

1
2
3
4
5
6
7
uci delete network.wg0_peer_vps.allowed_ips 2>/dev/null

uci add_list network.wg0_peer_vps.allowed_ips='10.77.0.0/24'
uci set network.wg0_peer_vps.route_allowed_ips='1'

uci commit network
/etc/init.d/network reload

如果将来某远端子网也通过 VPS 可达,再按需添加,例如:

1
2
3
uci add_list network.wg0_peer_vps.allowed_ips='192.168.50.0/24'
uci commit network
/etc/init.d/network reload

当前不建议把工位 192.168.50.0/24 交给 WireGuard,除非工位网关也接入 WireGuard。

5.6 配置 OpenWrt 防火墙

创建 vpn zone,并允许 lan <-> vpn

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
uci -q delete firewall.vpn
uci -q delete firewall.lan_to_vpn
uci -q delete firewall.vpn_to_lan

uci set firewall.vpn='zone'
uci set firewall.vpn.name='vpn'
uci add_list firewall.vpn.network='wg0'
uci set firewall.vpn.input='ACCEPT'
uci set firewall.vpn.output='ACCEPT'
uci set firewall.vpn.forward='ACCEPT'
uci set firewall.vpn.masq='0'
uci set firewall.vpn.mtu_fix='1'

uci set firewall.lan_to_vpn='forwarding'
uci set firewall.lan_to_vpn.src='lan'
uci set firewall.lan_to_vpn.dest='vpn'

uci set firewall.vpn_to_lan='forwarding'
uci set firewall.vpn_to_lan.src='vpn'
uci set firewall.vpn_to_lan.dest='lan'

uci commit firewall
/etc/init.d/firewall restart

检查:

1
uci show firewall | grep -E 'vpn|wg0'

5.7 OpenWrt 测试

在 OpenWrt 上:

1
2
3
4
5
wg show
ip addr show wg0
ip route | grep 10.77

ping -c 4 10.77.0.1

从 VPS 测:

1
2
ping -c 4 10.77.0.2
ping -c 4 192.168.1.1

如果 10.77.0.2 通,但 192.168.1.1 不通,优先查:

1
2
3
VPS 上 dorm-openwrt 的 AllowedIPs 是否包含 192.168.1.0/24
OpenWrt 防火墙是否允许 vpn -> lan
VPS 是否开启 net.ipv4.ip_forward

6. 工位 Linux 单设备 Peer

6.1 安装

Ubuntu / Debian:

1
2
sudo apt update
sudo apt install -y wireguard

Rocky / RHEL / Fedora:

1
sudo dnf install -y wireguard-tools

6.2 生成密钥

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
sudo install -d -m 700 /etc/wireguard

sudo bash -c '
set -e
umask 077

if [ ! -f /etc/wireguard/client_private.key ]; then
 wg genkey > /etc/wireguard/client_private.key
fi

wg pubkey < /etc/wireguard/client_private.key > /etc/wireguard/client_public.key

chmod 600 /etc/wireguard/client_private.key
chmod 644 /etc/wireguard/client_public.key
'

查看公钥:

1
sudo cat /etc/wireguard/client_public.key

把输出的 OFFICE_LINUX_PUBLIC_KEY 加到 VPS。

6.3 Linux 配置 /etc/wireguard/wg0.conf

1
sudo nano /etc/wireguard/wg0.conf

配置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
[Interface]
# office-linux-thinkstation
Address = 10.77.0.101/32
PrivateKey = OFFICE_LINUX_PRIVATE_KEY
SaveConfig = false

[Peer]
# rocky-vps
PublicKey = VPS_PUBLIC_KEY
Endpoint = VPS_PUBLIC_IP_OR_DOMAIN:51820
AllowedIPs = 10.77.0.0/24, 192.168.1.0/24
PersistentKeepalive = 25

说明:

  • 10.77.0.0/24 用于访问 WireGuard 内所有 peer。
  • 192.168.1.0/24 用于访问宿舍 LAN。
  • 如果只需要访问 WireGuard peer,不需要访问宿舍 LAN,可以删掉 192.168.1.0/24
  • 如果短期只在同一校园网内测试,可以临时把 Endpoint 写成 VPS 当前内网可达 IP;长期使用应改成 VPS 公网 IP 或稳定域名。

权限:

1
sudo chmod 600 /etc/wireguard/wg0.conf

启动:

1
sudo systemctl enable --now wg-quick@wg0

重启:

1
sudo systemctl restart wg-quick@wg0

检查:

1
2
3
sudo wg show
ip addr show wg0
ip route | grep -E '10\.77|192\.168\.1'

测试:

1
2
3
ping -c 4 10.77.0.1
ping -c 4 10.77.0.2
ping -c 4 192.168.1.1

7. Windows 单设备 Peer

7.1 WireGuard 客户端配置

安装 Windows 官方 WireGuard 客户端后:

1
Add Tunnel -> Add empty tunnel

客户端会自动生成 PrivateKeyPublicKey

Windows 配置模板:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
[Interface]
# office-windows
PrivateKey = OFFICE_WINDOWS_PRIVATE_KEY
Address = 10.77.0.102/32

[Peer]
# rocky-vps
PublicKey = VPS_PUBLIC_KEY
Endpoint = VPS_PUBLIC_IP_OR_DOMAIN:51820
AllowedIPs = 10.77.0.0/24, 192.168.1.0/24
PersistentKeepalive = 25

VPS 上添加:

1
2
3
4
[Peer]
# office-windows
PublicKey = OFFICE_WINDOWS_PUBLIC_KEY
AllowedIPs = 10.77.0.102/32

应用 VPS 配置:

1
sudo systemctl restart wg-quick@wg0

7.2 Windows 测试

PowerShell:

1
2
3
ping 10.77.0.1
ping 10.77.0.2
ping 192.168.1.1

注意:

1
2
3
Windows 防火墙经常默认拦 ICMP。
别人 ping Windows 的 10.77.0.102 不通,不一定代表 WireGuard 错。
RDP、SSH、HTTP 等实际服务端口测试更可靠。

8. Clash / v2ray TUN 与 WireGuard 共存

8.1 问题现象

TUN 模式代理可能截获 WireGuard 外层 UDP 流量:

1
本机 -> VPS_PUBLIC_IP:51820/udp

如果这条流量被代理接管,常见现象:

1
2
3
4
WireGuard latest handshake 不更新
transfer 只有 sent,没有 received
ping 10.77.0.1 不通
开 TUN 就断,关 TUN 就恢复

8.2 规则原则

必须 DIRECT

1
2
3
4
5
6
7
VPS_PUBLIC_IP/32
UDP 51820
10.77.0.0/24
10.88.0.0/24
192.168.1.0/24
192.168.50.0/24
RFC1918 私网

Clash 规则示例:

1
2
3
4
5
6
7
8
rules:
 - IP-CIDR,VPS_PUBLIC_IP/32,DIRECT,no-resolve
 - IP-CIDR,10.77.0.0/24,DIRECT
 - IP-CIDR,10.88.0.0/24,DIRECT
 - IP-CIDR,192.168.1.0/24,DIRECT
 - IP-CIDR,192.168.50.0/24,DIRECT
 - GEOIP,PRIVATE,DIRECT
 - MATCH,PROXY

建议 WireGuard Endpoint 用 IP,不用域名:

1
Endpoint = VPS_PUBLIC_IP:51820

这样可以避免 DNS 查询被 TUN 代理影响。若必须用域名,则 DNS 也要确认不被代理规则错误接管。


9. Wake-on-LAN 远程唤醒宿舍 PC

9.1 正确链路

推荐链路:

1
2
3
4
5
6
7
远端设备
 -> WireGuard
 -> VPS
 -> WireGuard
 -> 宿舍 OpenWrt 10.77.0.2
 -> OpenWrt 在 br-lan 发送 WOL 魔术包
 -> 唤醒宿舍 Windows PC

不要优先尝试远端直接向 192.168.1.255 广播。WireGuard 是三层隧道,广播不一定穿透。最稳的是让 OpenWrt 在宿舍 LAN 本地发 WOL。

9.2 Windows PC 设置

BIOS / UEFI:

1
2
3
Wake on LAN: Enabled
Power On By PCI-E / Resume By PCI-E Device: Enabled
ErP / EuP / Deep Sleep: Disabled

MSI 主板进 BIOS:

1
2
开机连续按 Delete
或 Windows 执行:shutdown /r /fw /t 0

Windows 网卡设置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
设备管理器 -> 网络适配器 -> 有线网卡 -> 属性

电源管理:
 允许此设备唤醒计算机:开启
 只允许魔术封包唤醒计算机:开启

高级:
 Wake on Magic Packet:Enabled
 Shutdown Wake-On-Lan:Enabled
 Wake from S5:Enabled
 Wake on Pattern Match:建议关闭或不依赖

如果要从关机状态唤醒,建议关闭 Fast Startup:

1
控制面板 -> 电源选项 -> 选择电源按钮的功能 -> 更改当前不可用设置 -> 取消“启用快速启动”

如果只从睡眠唤醒,可以先不关 Fast Startup,先把最小链路测通。

9.3 OpenWrt 安装 WOL 工具

1
2
opkg update
opkg install etherwake luci-app-wol

只用命令行:

1
opkg install etherwake

9.4 查询 Windows PC MAC

Windows:

1
ipconfig /all

找有线网卡:

1
Physical Address / 物理地址

OpenWrt 上也可以查:

1
2
cat /tmp/dhcp.leases
ip neigh show dev br-lan

MAC 格式统一写成:

1
AA:BB:CC:DD:EE:FF

9.5 手动唤醒

OpenWrt 上执行:

1
etherwake -i br-lan AA:BB:CC:DD:EE:FF

宿舍 LAN 接口是:

1
br-lan

不要从 wg0pppoe-wan 发 WOL。

9.6 LuCI WOL 设置

如果使用 LuCI:

1
2
3
4
服务 -> Wake on LAN
Interface: br-lan
MAC: Windows 有线网卡 MAC
Send to broadcast address: 开启

建议开启 Send to broadcast address

理由:

1
2
PC 睡眠或关机后,OpenWrt 可能没有它的 ARP 记录。
广播发送更容易被网卡收到。

不要做:

1
2
不要从 WAN 端口转发 UDP 7/9 到 LAN
不要允许公网直接发 WOL 广播

9.7 固定唤醒脚本

1
2
3
4
5
6
cat > /root/wake-dorm-pc.sh <<'EOF'
#!/bin/sh
etherwake -i br-lan AA:BB:CC:DD:EE:FF
EOF

chmod +x /root/wake-dorm-pc.sh

远程唤醒:

1
ssh [email protected] /root/wake-dorm-pc.sh

宿舍内网唤醒:

1
ssh [email protected] /root/wake-dorm-pc.sh

9.8 WOL 排障顺序

先测试睡眠唤醒:

1
2
Windows PC 睡眠
OpenWrt 执行 etherwake

再测试关机唤醒。

如果睡眠能醒、关机不能醒,优先检查:

1
2
3
4
5
BIOS ErP 是否关闭
BIOS Resume By PCI-E 是否开启
Windows Fast Startup 是否关闭
网卡高级选项 Shutdown Wake-On-Lan 是否开启
PC 是否使用有线网卡

如果完全不能醒,优先检查:

1
2
3
4
MAC 是否是有线网卡 MAC
PC 是否接在 br-lan 下
网口关机后灯是否亮
网线、交换机、路由器 LAN 口是否正常

10. 常用排障命令

10.1 WireGuard 通用检查

1
2
3
sudo wg show
ip addr show wg0
ip route

重点看:

1
2
3
4
latest handshake: 是否更新
transfer: 是否双向增长
allowed ips: 是否正确
endpoint: 是否是正确 VPS 地址

典型问题:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
只有 sent,没有 received:
 对方没添加 peer
 公钥不匹配
 Endpoint 不通
 防火墙或云安全组没放行 UDP 51820

有 handshake,但 ping 不通:
 路由或 AllowedIPs 错
 IP 转发没开
 firewalld / OpenWrt 防火墙拦截
 Windows 防火墙拦 ICMP

10.2 VPS 检查

1
2
3
4
5
6
sudo wg show
ip route | grep -E '10\.77|10\.88|192\.168\.1|192\.168\.50|default'
sysctl net.ipv4.ip_forward
sudo firewall-cmd --list-ports
sudo firewall-cmd --get-active-zones
sudo systemctl status wg-quick@wg0 --no-pager

日志:

1
sudo journalctl -u wg-quick@wg0 -n 100 --no-pager

应用配置:

1
sudo systemctl restart wg-quick@wg0

10.3 OpenWrt 检查

1
2
3
4
5
6
wg show
ip addr show wg0
ip route | grep -E '10\.77|192\.168\.1'
uci show network.wg0
uci show network.wg0_peer_vps
uci show firewall | grep -E 'vpn|wg0'

重载网络:

1
/etc/init.d/network reload

重启防火墙:

1
/etc/init.d/firewall restart

10.4 Linux peer 检查

1
2
3
4
5
6
sudo wg show
ip addr show wg0
ip route | grep -E '10\.77|192\.168\.1'
ping -c 4 10.77.0.1
ping -c 4 10.77.0.2
ping -c 4 192.168.1.1

重启:

1
sudo systemctl restart wg-quick@wg0

10.5 连通性测试顺序

从工位 Linux 测:

1
2
3
ping -c 4 10.77.0.1 # VPS
ping -c 4 10.77.0.2 # 宿舍 OpenWrt WG
ping -c 4 192.168.1.1 # 宿舍 OpenWrt LAN

从 VPS 测:

1
2
3
ping -c 4 10.77.0.2
ping -c 4 192.168.1.1
ping -c 4 10.77.0.101

从宿舍 OpenWrt 测:

1
2
ping -c 4 10.77.0.1
ping -c 4 10.77.0.101

判断顺序:

1
2
3
1. 先确认 peer 到 VPS 的 10.77.0.1 通。
2. 再确认 peer 之间的 10.77.0.x 通。
3. 最后确认跨到宿舍 LAN 的 192.168.1.0/24 通。

不要一开始就测 RDP、SSH 或 WOL。先把三层连通性验证完。


11. 新增 peer 标准流程

11.1 选择 IP

建议:

1
2
3
4
手机: 10.77.0.10/32
笔记本: 10.77.0.11/32
工位 Linux: 10.77.0.101/32
工位 Win: 10.77.0.102/32

11.2 在新设备生成密钥

Linux:

1
2
3
umask 077
wg genkey > private.key
wg pubkey < private.key > public.key

Windows / macOS / 手机:

1
2
WireGuard App -> Add empty tunnel
自动生成 PrivateKey / PublicKey

11.3 VPS 添加 peer

1
2
3
4
[Peer]
# device-name
PublicKey = DEVICE_PUBLIC_KEY
AllowedIPs = 10.77.0.x/32

重启 VPS:

1
sudo systemctl restart wg-quick@wg0

11.4 客户端配置

单设备客户端模板:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[Interface]
Address = 10.77.0.x/32
PrivateKey = DEVICE_PRIVATE_KEY
SaveConfig = false

[Peer]
PublicKey = VPS_PUBLIC_KEY
Endpoint = VPS_PUBLIC_IP:51820
AllowedIPs = 10.77.0.0/24, 192.168.1.0/24
PersistentKeepalive = 25

如果只想访问 WireGuard peer,不访问宿舍 LAN:

1
AllowedIPs = 10.77.0.0/24

如果想全局代理,才写:

1
AllowedIPs = 0.0.0.0/0, ::/0

当前不建议默认全局代理。这个方案的目标是远程组网和运维,不是替代所有代理流量。


12. 安全注意事项

12.1 私钥管理

1
2
3
4
PrivateKey 永远只留在本机。
PublicKey 可以复制给 VPS。
不要把 PrivateKey 发到聊天、GitHub、博客、截图。
如果私钥泄露,重新生成该 peer 的密钥,并更新 VPS 上的 PublicKey。

12.2 公网暴露面

VPS 只需要暴露:

1
UDP 51820

不要为了 WOL 暴露:

1
2
3
UDP 7
UDP 9
192.168.1.255 广播转发

WOL 只从 OpenWrt 的 br-lan 本地发。公网只允许进入 WireGuard。

12.3 普通 LAN 设备是否需要安装 WireGuard

宿舍 192.168.1.0/24 下的普通设备不需要安装 WireGuard,也可以访问 10.77.0.0/24,前提是:

1
2
3
4
默认网关是 192.168.1.1
OpenWrt route_allowed_ips=1
OpenWrt 防火墙允许 lan -> vpn
VPS 允许转发

如果普通设备自己也安装 WireGuard,可以,但它只能宣告自己的 /32,不能宣告整个 LAN。

12.4 不要让普通终端假装网关

工位 Linux/Windows 如果只是单设备 peer,就只写:

1
2
AllowedIPs = 10.77.0.101/32
AllowedIPs = 10.77.0.102/32

不要写:

1
AllowedIPs = 192.168.50.0/24

除非这台设备真的是工位 LAN 的默认网关,并且已经配置好 IP 转发、防火墙和回程路由。


13. 当前推荐状态总结

应保持:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
VPS:
 10.77.0.1/24
 peer dorm-openwrt: 10.77.0.2/32, 192.168.1.0/24
 peer office-linux: 10.77.0.101/32
 后续 peer office-windows: 10.77.0.102/32

OpenWrt:
 wg0 10.77.0.2/32
 LAN 192.168.1.1/24 on br-lan
 AllowedIPs to VPS: 10.77.0.0/24
 firewall: lan <-> vpn

Office Linux:
 wg0 10.77.0.101/32
 AllowedIPs: 10.77.0.0/24, 192.168.1.0/24

Office Windows:
 wg0 10.77.0.102/32
 AllowedIPs: 10.77.0.0/24, 192.168.1.0/24

WOL:
 OpenWrt br-lan 发 etherwake
 远程命令: ssh [email protected] /root/wake-dorm-pc.sh

不要做:

1
2
3
4
5
6
不要让 RT-AC88U 386.14_2 强行跑 WireGuard 主链路。
不要把 192.168.50.0/24 交给工位 Linux/Windows 单设备 peer。
不要让 Clash/v2ray TUN 代理 VPS:51820 的 WireGuard 外层流量。
不要从公网转发 UDP 7/9 做 WOL。
不要在多个 peer 上重复宣告同一个 LAN 网段。
不要把 PrivateKey、真实 MAC、VPS 公网 IP 随手贴到公开文档。

14. 一页速查

VPS:

1
2
3
4
5
6
sudo wg show
sudo systemctl restart wg-quick@wg0
ip route | grep -E '10\.77|192\.168\.1|default'
sysctl net.ipv4.ip_forward
sudo firewall-cmd --list-ports
sudo firewall-cmd --get-active-zones

OpenWrt:

1
2
3
4
5
6
wg show
ip route | grep -E '10\.77|192\.168\.1'
uci show network.wg0_peer_vps
uci show firewall | grep -E 'vpn|wg0'
/etc/init.d/network reload
/etc/init.d/firewall restart

Linux peer:

1
2
3
4
5
sudo wg show
ip route | grep -E '10\.77|192\.168\.1'
ping -c 4 10.77.0.1
ping -c 4 10.77.0.2
ping -c 4 192.168.1.1

WOL:

1
ssh [email protected] /root/wake-dorm-pc.sh

如果只记一个排障原则:

1
2
3
4
先看 latest handshake 和 transfer 是否双向增长;
再看 VPS AllowedIPs;
再看 IP 转发;
最后看两端防火墙和本机代理 TUN 规则。

随想 260529_AI

2026-05-29 06:45:00

Featured image of post 随想 260529_AI

失眠与熬夜:红温

敲下了标题,已是凌晨 6:47,一夜未眠,先去吃个早饭。

忘了拍照,雪菜粉丝包和豆腐脑还是比北京的好吃多了,倒是鸡蛋灌饼没冬天热乎好吃了,应该是换了厨子。

经典睡不着红温,索性下床学习通宵算了。好吧,倒也没有那么经典,上次还是大三吧,不过这次不能下床就趴在桌前敲笔记本,得跑去工位了。

其实说实话有点傻逼真的,自以为摆脱思想桎梏的人现在天天晚上为世俗的前途焦虑地睡不着觉,最喜欢玩的人迷上了 Vibe Coding 后一个月没打开过游戏。归根结底,还是被困在了这个毫无希望的学校里。

AI:再谈 AI

回归正题,敲完这篇就回去睡觉了。

对大模型的接触自然要追溯到 2022 年末的 ChatGPT 了,它从 11 月 30 日发布到突破 100 万用户只用了短短五天。我的第一个账号也是在当时注册的,一篇知乎让我花了一个半小时从接码到注册用上了大模型。

ChatGPT

ChatGPT

用上 GPT 后的第一个“工程实践”就震惊了我,它不仅能够根据 Java 老师的作业要求给出能跑的代码,甚至还会授人以渔和代码纠错,这对当时只会机械执行一行行代码的机器人般的我来说简直是个奇迹!

ChatGPT

说实话,后面的一年里,GPT 和其他大模型于我只有一个作用:水掉作业。于是在疫情宅家的日子里,我不是在打游戏就是在打游戏,与此同时 DaleChu 同学似乎在那段时间深耕了键政和计算机技术。

时间一晃来到了 2023 年的九月开学,在 lihan 开始模仿 DaleChu 的博客时,对大模型的使用也慢吞吞地进入了下一个阶段——代码补全。申请了 GitHub Copilot 的学生会员后,代码补全 + ChatGPT 的组合一方面让刚准备开始发奋学习技术的我得以更快上手,另一方面也让我第一次意识到似乎手搓代码就跟提笔写字一样要被淘汰了。

copilot

自此之后,一直到研一,我对大模型的使用与研究便停滞于此了,从 GPT-4 到 GPT-5,对我而言不过是一次次版本号的迭代,以及模型能力的日常提升。除了越来越多人开始使用大模型,越来越多的垂类产品也开始涌现之外,并没有什么特别的感觉。当然,这也源于我对当时那些所谓的“Prompt 工程”的不屑一顾了。如今再看,真的很难想象自己居然纯靠网页 ChatGPT 完成我的 DSSE 毕业设计。

AI 的热情再度被点燃是在研一的十月份了,在 lyx 的教导下,我开始试着接触科研和实验。第一次看到他用 Copilot Vibe Coding 写代码时我简直像极了面对流水线鲜花饼产线的老手艺人——还能这样?

现在说什么都来不及了,Vibe Coding 沾上了就戒不掉了,纵使一开始对失去项目与代码掌控权的疑虑到现在还没完全打消,我已然是无法回到最开始的手搓代码了。

lemoncchi 说,大模型害惨了我们这些没跟上这趟科研快车的程序员。诚然,倘若没有大模型的出现,我们仍然能够凭借编程的技术,在刷 LeetCode、背面经后,一路实习、面试,最终找到一份凭借着剩余价值转移得以维持高薪的码农工作。但现在,可见的未来,简历上的“熟悉 C++”几乎和“熟悉 WPS”要画上等号了,更可恶的是,精心(也许精心吧嘿嘿)维护的博客里的技术贴已经完全失去意义了,也再不会有无知美少女抱着电脑一脸崇拜地请教你某个小技术问题了(说实话感觉主要是 DaleChu 和 lemoncchi 在经历这个,我只能给壮汉讲呜呜呜)。

但倘若不从功利的角度出发,大模型的出现几乎是一个让人美梦成真的奇迹!今年初开始让老程序员们重燃青春的 OpenClaw 和 Hermes,让无数像家父和我导师这样的老程序员们上头了几个月还没退烧,纵使一个月几千的 Token 账单贴在脸前,一个真的能把你的想法实现的魔法机器摆在面前,谁能不心动呢?是啊,记得小时候,看着网上所谓“脚本小子”为游戏制作外挂,听起来好像得学好多年才能到这个地步,现如今十几分钟就能让大模型给我用 YOLOv8 跑起来挂机脚本了;困扰了我两年的博客音乐列表全自动更新功能,也不过是一杯咖啡的功夫就能给我完美实现了。大模型的出现对于不考虑以此谋生的程序员来说,真的是原神了。

中转站:又上头了

导师其实早就紧跟时代了,给我们发模型让我们用 ClaudeCode 做横向项目了,从他的 GitLab 记录来看,过年期间他也上了 OpenClaw 这趟车,直到今天还在兴致满满地安排着他的 Agents 用 Vibe Coding 做各种项目。相比之下,我这个老古董学生倒是在 Copilot 报废后,才勉强开始把他发的 GLM 接入 ClaudeCode 使用。

用 ClaudeCode 的第一天就把卡了我几周的 Bug 轻松秒掉了。

今年三月左右开始,让我白嫖了多年的 Copilot 开始抠搜了,先是旗舰模型被移入更高价位的套餐,然后是使用次数被一次又一次限制,月限、周限、五小时限,一直不想“自费打工”的我总算在四月尾巴坚持不住,开始转向 Codex 和 ClaudeCode 了。

2026 年 4 月 27 日,面对额度耗尽的 GLM,lihan 打开了当年偷瞄 DaleChu 屏幕看到的那个神奇论坛——LinuxDO,在通宵达旦地高强度刷了一天帖子后,终于跟上了这波“中转站 - Vibe Coding - AI Workflow”浪潮。在几个中转站里摸爬滚打了一两天后,从“福利羊毛”区的大善人们的投喂和“跳蚤市场”区的低价 GPT Team 车开始,走上了一趟狂热的 Vibe Coding 之旅。

linuxdo

在牺牲了原本备考数学期末的时间、用 Vibe Coding 给开源项目提交 PR 后,我很快意识到不能把精力砸在抠搜上——强大先进的生产力应当不计代价地立刻投入使用!于是在考完后的当天,我就开始通宵搭建我这个基于 New API + CliProxyAPI 的中转站 lihan API 了。

一扫以往买域名、买服务器、买节点的犹豫,花了五天时间,我便部署好中转站投入使用了。从一开始三天报销掉三个 GPT Plus 的周限,到现在后台额度波澜不惊,即便小小地给周围同学朋友推广使用、企图回血,也很少再出现额度不足的情况了,我这才开始缓慢地真正掌握 Coder Agent 这把尚方宝剑。

cpa

AI 随想:我不管,我就是对的

在 Vibe Coding 中的摸爬滚打难以言喻,总结下来,不过是以下几点:

  • 大模型是革命性的生产力提升,早点投降开始 Vibe Coding 才是正道
  • Vibe Coding 拉低了 Idea 实现的门槛,什么垃圾想法都能硬控我浪费时间精力和 Token,一定要让时间精力花在真正的好想法上
  • 不要在 Vibe Coding 中迷失原本的需求,用 AI 工具要最快最专业最轻量地实现你的需求
  • Vibe Coding 不是门槛级别的技术,长期的投入仍然要聚焦在某一点上
  • 传统开发岗 G 了,搞 Agent 才能真正算是程序员了
  • 始终将社区最好的解决方案、最好的模型拿来就用,不要自己重复造轮子
  • 一个自己的 AI Workflow 是必要的

结语

希望年末我能笑着去香港喜提 MacBook Pro M6 Pro 和 iPhone 18 Pro Max!

Codex 从 Mac 远连 Windows——绕过 pwsh,走 WSL 的 sshd

2026-05-21 00:00:00

Featured image of post Codex 从 Mac 远连 Windows——绕过 pwsh,走 WSL 的 sshd

本文是一次真实配置的完整走查记录(一次性、面向人阅读)。这套环境配好一次就长期 复用,我大概率不会再从头踩坑,所以收成博文而不是知识卡片。

背景与问题

想用 Codex 的 remote-connect 功能从 Mac 驱动一台 Windows 机器。连上之后,Codex 在远端跑 bootstrap 一上来就失败:报 pwsh 看不懂的 token、&&$(...) 之类的 错误,或者客户端报远端 shell 没返回预期的握手。

根因是 shell 不匹配

  • Codex 的远端 bootstrap 假定远端是 POSIX shell(sh / bash)。
  • Windows 自带的 OpenSSH server 默认把 pwsh(或 cmd)当登录 shell,所以那些 一行式 bootstrap 命令在 Codex 启动之前就先炸了。
  • 同一台 Windows 上的 WSL Ubuntu 里有真正的 bash。解法就是把 SSH 会话落进 WSL,而不是落在 Windows 本体。

适用场景:用 Mac 通过 Codex remote-connect(或其他假定 sh 的远程开发工具,比如 JetBrains Gateway、bootstrap 脚本只认 sh 的 VS Code Remote-SSH)去驱动一台装了 WSL 的 Windows 机器,且连接在 bootstrap 阶段挂在 shell 不兼容 / pwsh / command-not-found 上。

不适用:远端本身就是原生 Linux(没有 shell 不匹配)、远程工具明确支持 Windows 侧 pwsh、或者那台 Windows 上压根没装 WSL 发行版。

四步配置

第 1–3 步每台 Windows 主机一次性;第 4 步每台 Mac 一次性。

1. 在 WSL Ubuntu 里装 sshd

在 Windows 上打开 WSL Ubuntu:

1
2
3
sudo apt update
sudo apt install -y openssh-server
sudo service ssh start

确认:

1
2
which sshd
sudo service ssh status

2. 把 Mac 公钥写进 WSL 的 authorized_keys

在 Mac 上:

1
cat ~/.ssh/id_ed25519.pub

复制整行。进到 WSL 里:

1
2
3
4
mkdir -p ~/.ssh
chmod 700 ~/.ssh
nano ~/.ssh/authorized_keys # 粘贴 Mac 公钥,保存
chmod 600 ~/.ssh/authorized_keys

3. 把 Windows 的 :2222 转发到 WSL 的 :22

在 Windows 上以管理员 PowerShell 执行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
$wslIp = (wsl hostname -I).Trim().Split()[0]

netsh interface portproxy add v4tov4 `
 listenaddress=0.0.0.0 `
 listenport=2222 `
 connectaddress=$wslIp `
 connectport=22

New-NetFirewallRule `
 -DisplayName "WSL SSH 2222" `
 -Direction Inbound `
 -Action Allow `
 -Protocol TCP `
 -LocalPort 2222

4. 在 Mac 的 SSH config 里加一个 Host 条目

编辑 Mac 上的 ~/.ssh/config

1
2
3
4
5
Host lihan_pc_02_wsl
 HostName <windows-host-lan-ip> # 例如 10.88.0.6
 Port 2222
 User <wsl-username>
 IdentityFile ~/.ssh/id_ed25519

然后在 Codex 里把 remote-connect 指向 Host 别名 lihan_pc_02_wsl——Codex 看到 的是一个 POSIX 远端,bootstrap 就能干净跑通。

踩坑点

  • WSL IP 每次重启都会漂。 第 3 步把 $wslIp 当成一个快照写死了。一旦 wsl --shutdown 或 Windows 重启,要重跑 netsh interface portproxy add (先用 netsh interface portproxy delete v4tov4 listenaddress=0.0.0.0 listenport=2222 删掉旧的)。可以把它写成一个开机自动跑的计划任务。
  • 别把代理塞进 git config。 这台机器上别处用的 HTTP 代理 (10.88.0.6:10808)跟这条 SSH 路径无关,别混在一起。
  • 连进 WSL,不是连进 Windows。 如果 Codex 客户端直接落在 Windows 本体的 22 端口,你又回到 pwsh 失败的原点了。整套方案的意义就是用 2222 → WSL:22 这一跳,强制把会话顶进 WSL。

怎么确认连通

在 Mac 上:

1
ssh lihan_pc_02_wsl -o BatchMode=yes -o ConnectTimeout=5 'uname -a'

返回一个 Linux 内核串(形如 Linux ... microsoft-standard-WSL2)且退出码为 0,就说明这台 Host 上的 Codex remote-connect 能用了。

连不上时按顺序排查:

  1. WSL sshd 起没起:sudo service ssh status
  2. Windows portproxy 是否还指向当前 WSL IP:对比 netsh interface portproxy show v4tov4wsl hostname -I (每次 WSL 重启 IP 都会漂);
  3. 防火墙规则 WSL SSH 2222 是否存在;
  4. ~/.ssh/authorized_keys 里有没有 Mac 公钥,且权限是 600。

AI 时代人机协作工作流博客 V4 说明

2026-05-13 20:50:00

Featured image of post AI 时代人机协作工作流博客 V4 说明

先说结论

这个博客现在进入 V4:AI 时代人机协作工作流博客

听上去有点大。核心很朴素:过去这里是赛博日记、技术学习笔记、灵感收纳盒;现在 AI 已经能帮我查资料、写代码、跑命令、改项目,旧式"把技术步骤写成长教程以备下次翻"的价值断崖式下降——我自己都不再回去翻那些教程,模型一句话就把活干了。

所以 V4 起,博客分两条线:

  1. 给人看的内容继续保留人的味道:随想、旅行、生活、阶段总结,该怎么写还怎么写。
  2. 给 AI 看的技术内容改造成工具:一种叫"卡片(card)“的小文件,每张装一段可复用的经验,让未来的 AI 在任何项目里都能直接拿来用。

不是把"小窝"装修成企业文档站。更像是在小窝里加了一个工具间,里面放着扳手、标签、清单和一点点家规——给我自己和与我合作的模型。

旧博客不动

旧博文全保留,不迁移、不重写、不批量整理 frontmatter。

旧文章里有前 AI 时代的学习痕迹,有当时看起来笨但很真实的记录。它们不是脏数据,不需要被 AI 打扫得光滑如新

以后写生活、旅行、随想、计划,仍然是传统博客文章。AI 帮我润色、排版、检查链接 OK,但文章本身是给人读的。

5 天后:最初的设想砍了大半

说实话,V4 刚开局时我画的图比现在大得多。

我最初想了一堆东西:4 种资产格式(tip / playbook / skill / session),一套 GitHub dispatch 自动管道,几类 hook 在敏感时刻插手,甚至给本地 MCP server 留好了位置——准备把博客做成一个能查能写能复盘的"小型知识库平台”。

V4 上线 5 天,一边用一边改。这一周下来,最初设想里一大半工程没保留下来

  • 4 种资产格式 → 1 种:「卡片」。tip / playbook 都是它的 type 字段。
  • GitHub dispatch 自动管道 → 删了。所有发布走人工 PR。
  • hooks 干预 → 没建。守护进程会模糊"AI 做了什么"的边界,违反隐私默认。
  • 本地 MCP server → 暂缓curl + jq 还够用。

砍到最后,剩下一个 静态 JSON 索引(registry) + 一种 markdown 格式(card) + 一个跨工具的薄 skill

这种"塌缩"不是失败。是发现 大多数工程感强的设计其实在替我加包袱,而真正起作用的是那条很短的链路:

1
AI 查 registry → 看摘要决定要不要打开 → 至多打开一张正文 → 干活

整套工程在原仓库里有一份对应的"宪法"和几条硬底线在背书——不允许 always-on infrastructure,不允许 agent sprawl(一项功能两套实现并存),不允许批量加载,等等。每次我想"再加一个守护进程"或"再加一种资产格式",那几条底线把我拍回去。结果就是越用越简单。

一种格式:卡片

卡片现在就是一个 markdown 文件,写在 ai/cards/<id>.md,frontmatter + 紧凑正文。

frontmatter 里固定写:idtypesummarystatus(valid/stale/retired)、context_costlast_verified,最关键一项是 verify——三段:执行什么 / 期望看到什么 / 看不到怎么办。这一段直接决定一张卡的可复用性。

正文必填三段:

1
2
3
## Trigger 什么场景下加载这张
## Fix 具体怎么做
## Reuse Rule 加载条件 / 不加载条件

整张卡片故意写得"对人不友好"——bullet、表格、key-value,没有起承转合。它的读者是模型,不是访客。完整模板与契约见 /ai/ 页面。

一个公开 URL

模型查经验只有一个入口:

1
https://lihan3238.github.io/ai/registry.json

不需要 clone 仓库,不需要装 SDK,不需要登录。一个静态 JSON 暴露在公网上,谁来都能查。

URL 就是协议。如果某天有更好的模型,它能用;如果某天我换了主力工具,它也能用。这种"少依赖"是这个层目标活久一点的唯一办法。

趋势:这种层在变成标配

回过头看,V4 砍掉那些花活之后,剩下的东西不太像"我自创的工作流"——更像是 AI 工具圈这两年慢慢涌现的标配:

  • 经验沉淀成 可查询的、机器可读的小文件,而不是长教程。
  • 协议长在 公开静态资源 上,不长在某个特定工具/IDE/订阅 里。
  • 自动化只在 任务收尾的瞬间 由模型主动建议,所有写入需要人点头,不靠后台扫描。
  • 工具适配层(skill / plugin / extension)只是一层薄壳,真东西在卡片里——这样跨工具的迁移成本接近零。

我把这些写下来不是因为它们是我发明的。是因为我经过砍掉自己一堆"工程感很强的设计"之后,终于看清这条主流路径的轮廓。接下来这一年大概会越来越像这样

几条家规(这些是保留下来的)

  • 旧文章只读content/post/** 里的历史文章不要被批量迁移、重写、整理。只有当我明确点名某篇要改时,AI 才动。
  • 默认脱敏公开。卡片只保存公开可读、已脱敏的内容;不写原始对话、不写密钥、不写私密路径。
  • AI 资产要短小。一个 tip 能解决,就不要写成长 playbook;一个 playbook 能表达,就不要升级成 skill。“少读” 比 “全面” 更重要。
  • 更新完要说人话。每次功能 / 自动化 / 工作流更新完,AI 都要告诉我:这次改了什么、我怎么用、未来 AI 怎么调用、用哪个命令验证。

这几条会写进项目指令和质量门里,防止"改完功能丢一句’完成了’就跑路"。

这次改造的意义

我觉得这一版最大的变化,不是多了几个目录、多了几个自动化脚本。

真正的变化是:博客不再只是"我写给未来的我看",而是变成"我和未来的 AI 一起工作的接口"

过去把知识写下来是怕忘。现在把流程固化下来是为了让人和 AI 都能更稳一点地进化。

而且——这套接口已经把我自己以前画的复杂版砍掉了大半。如果半年后我回头再看,可能还会再砍一些。没关系,越简单越像主路

当然,小窝还是小窝。技术层长出了一套机器骨架,门口仍然可以放歌、写随想、贴旅行照片、偶尔吐槽人生。这样就挺好。

更新(2026-06-12):一个月后,重心从"卡片"移到了"运行时"

V4 上线一个月,又砍了一轮、也长了一轮。上面正文保持原样,这里记录变化。

当初说"真东西在卡片里"。用了一个月发现不完全对:真东西在工作流里,卡片只是它的公开切片。所以这次重构把日常运行时彻底搬回本地——

  • 模式按仓库粘住。每个仓库是工程 / 科研 / 通用哪种模式,记在一个用户级 YAML 里(repo-modes.yaml),AI 不再每次会话重新猜。博客仓库永远是工程模式,研究仓库永远是科研模式,说一次就够。
  • 科研工作流收编。原先为研究工作零散长出的五个 skill(环境变量、运行时、笔记维护、假设冲刺、论文写作)全部折叠进 lihan-cards 的 references,按任务只加载一片。“一个入口"的家规扛住了第一次真正的膨胀压力。
  • 私有事实和工作流分了家。工作区路径、GPU 主机、下载策略这种"固定但会变"的东西进一个非密 profile 文件随仓库同步;密钥永远只在本地。工作流引用里只写字段名,不写值。
  • “至多打开一张正文"退役了。它被一套更细的 context budget 取代:原则精要常驻、环境检查可以打包加载、incident 调试仍然保持窄。一刀切的规则让位给按类别给预算。
  • 卡片 PR 可以自动合并了。这是对"所有写入需要人点头"的一次有边界的放松:我在捕获时点一次头(go),之后 CI 全绿 + 改动严格限制在卡片 allowlist 内,PR 自动合并。点头从"每一步"变成"每一件事一次”,底线(非卡片变更必须人合)没动。

回头看正文里那句"如果半年后我回头再看,可能还会再砍一些”——只用了一个月。砍掉的是"每次都重新解释我是谁、在哪个仓库、干哪类活"的摩擦;留下来的还是那条短链路,只是链路的起点从公网 URL 挪到了本地 skill,公网那份变成了展示窗和异地备份。

越用越像一个运行时,越不像一个文档站。 大概这就是对的方向。