这周的主线是两件事:wenkangnas.local (192.168.1.4) 上一台虚拟机的 CPU 被吃满,一路查到 OpenClaw 网关的内存问题;以及把桌上 ESP32 小板依赖的中继服务从虚拟机搬到了 Mac 上,顺手把板子一直显示的 no link 查清楚。
NAS CPU 压力大:其实是 OpenClaw 网关在漏内存
NAS(4 核)load 到了 5.5–6.3,全是 CPU 压力,IO 压力很低。top 里是两个 qemu 进程,对上 VMM 里的两台虚拟机;大头是 192.168.1.244(2 vCPU / 4 GB,已经跑了 35 天)。登进去看,是 OpenClaw gateway 的 node 进程堆内存涨到了 --max-old-space-size=2048 的上限,Node 一直在 GC,VM 同时在大量 swap(swap 一度用到 3.9 GB),这些 swap IO 落到 NAS 上,NAS 的内核态 CPU 也跟着高。journal 里能看到它之前以 ABRT 崩过两次(10/11 和 10/14),大约三天一次,很符合堆耗尽。
处理:先手动重启网关(swap 3.9 GB → 0.9 GB),再从 2026.10.1-beta.2 升到 2026.10.4。过程不顺:openclaw update 失败了两次,都卡在"停止旧网关"这一步(它只给 90 秒,这台 2 核 VM 来不及),最后改成手动——先停服务,npm i -g,再 openclaw doctor --fix 和 openclaw update repair,光 repair 就跑了约 40 分钟。另外 /opt/node-* 下几个目录的属主是一个不存在的 UID,改成运行服务的用户之后才能自己升级;更新通道原本是 beta,改成了 stable(openclaw config set update.channel stable)。
升级后网关跑了约 20 小时,堆内存稳定在 680–715 MB;旧版本同样时长已经到 0.85–1.2 GB。但我没法说"修好了":2026.10.4 的 changelog 里没有一条明确写修了内存泄漏,只是观察到升级后不涨了。我对日志做了更细的比对,有一个推测:旧版本大约每 6 小时会卡住 6 分钟左右(CPU 满载、完全不响应),卡完后日志里有旧插件"还有 1 个调用没结束、清理挂着"的记录,可能是这些没回收的旧插件在逐次吃内存。每 6 小时卡住的是哪个任务,定时任务表里查不到,没定位。中途 VM 又被重启过一次(我给它加了内存),观察期要重新算,打算 10-19 前后再看一次,堆超过 1.2 GB 就算没修好。另外清掉了网关下 4 组闲置的 codex app-server 子进程,之后网关自己拉起了新的,确认能自动重建。
升级时还有一个顺带的发现:升级后首次启动的自检把 TOOLS.md 自动并进了 AGENTS.md(原文件备份后删除),我核对过内容一行没少,然后按路径单独提交了这次迁移,并把文档里指向 TOOLS.md 的引用改掉了。
NAS 内存分配:OpenClaw VM 加到 5 GB
顺着上面的问题查了 NAS 整体内存:约 19 GB,可用约 4.7 GB,已承诺内存(Committed_AS)超过了 CommitLimit,有点超卖;最大的几块是 OpenClaw VM 4.5 GB、一台 Windows 虚拟机 4.4 GB、iStoreOS 2.1 GB,还有群晖自己的 fileindexd 2.7 GB(Universal Search / Photos 的索引,不用全文搜索的话可以缩)。OpenClaw VM 内部可用只剩约 700 MB、swap 用了 2.1 GB,确实缺内存。
我先让它加到 6 GB,后来因为我有时还要开 Win10(4 GB),折中改成 5 GB。VMM 的开关机和改配置都能用命令行完成,大致是这样(会话里用的参数名是 vram_size,单位 MB):
sudo /usr/syno/bin/synowebapi --exec api=SYNO.Virtualization.API.Guest.Action method=shutdown version=1 guest_name=<VM名>
sudo /usr/syno/bin/synowebapi --exec api=SYNO.Virtualization.API.Guest method=set version=1 guest_name=<VM名> vram_size=6144
sudo /usr/syno/bin/synowebapi --exec api=SYNO.Virtualization.API.Guest.Action method=poweron version=1 guest_name=<VM名>
必须先关机再改。开机后 VM 里的服务大多 20 秒内起来,网关本身最慢,约 4 分钟后才开始监听。结论:这几台 VM 同时开的话,Win10 和另一台 Windows 不要一起开。
把 ESP32 小板的中继搬到 Mac,顺手查清 no link
桌上那块 ESP32-P4 的 HUD 板依赖一个 relay(store-and-forward,纯内存),原来跑在 192.168.1.244 的 podman 里。我把它搬到了 Mac 上:一个 podman pod,里面 relay 和 cloudflared 两个容器,加 podman 自动生成的 infra 容器;域名和 token 都没变,所以板子固件不用改。Mac 重启后 --restart=always 不保证拉起 podman machine,所以加了一个 launchd 任务每 60 秒检查一次(没起就 podman machine start,pod 里有容器挂了就 podman pod start),手动停掉 cloudflared 验证过会被拉回来。代价也很明确:Mac 关机或睡眠时板子就拿不到新数据了。
搬完板子一直显示 no link,一开始以为是搬迁引起的。排查和结论单独写成了一篇:板子一直显示 no link:根因是 DHCP 下发的网关。
顺带改了固件:把最后一份数据存进板子上空着的 7 MB storage 分区(最多 5 分钟写一次),Mac 关机后重启板子也能先显示上次的数据,header 标出数据有多旧;加了 Wi-Fi 图标(带 dBm);把 no link 细分成 no Wi-Fi / no internet / Mac offline,并加了两条自愈重启(连上过 Wi-Fi 之后 3 分钟拿不到 IP,或有 IP 但 10 分钟连不上外网)。第一次烧录后板子不停重启:新加的 header 每秒定时器比 net_start() 先跑,调 net_is_connected() 时事件组还没创建,断言失败;给它加了个判空就好了。还给每个 agent 的卡片加了会动的小徽标。
我还让 AI 把 agent 顺序改成过 OpenCode → Grok → Claude → Codex,看了之后觉得应该和电脑端保持一致,又整个 revert 回去了。
另一个小插曲:板子旁边那个 2K USB 摄像头本来是拿来让 AI “看"屏幕的,但 ffmpeg 的 avfoundation 不管按名字还是序号,都会选成我 iPhone 的连续互通相机。最后写了个几十行的 Swift 小工具 hudcam,按设备 uniqueID 取帧,已经放进了仓库。
cfssh:给人远程协助用的 Cloudflare Tunnel + SSH 小工具
这周把自己的小工具 cfssh 继续往前推了几版(v0.3.2 → v0.3.4)。它的用法是:被协助的人只需要输入自己的系统登录密码,工具在本机起一个只监听 127.0.0.1 的 sshd,再用 Cloudflare Tunnel 把它接出去,对方不用做端口映射。几个改动:
- 把"创建隧道/DNS/Access"和"本机起 sshd + 隧道"拆开。前者用我自己的 API 令牌做一次,产出一个预配文件(里面只有隧道令牌、服务令牌,不含 API 令牌),客户端只读这个文件,界面上只剩一个密码框。
- macOS 上预配文件放在
.app旁边会找不到(程序实际在.app/Contents/MacOS/里)。这是代码评审机器人指出来的,我让 AI 改了并加了测试,还在 Mac 上用新旧两版做了对照,旧版确实会掉进给我自己用的高级界面。 - 断线自动重连,遇到的问题和做法写在这篇里。
- 补了
config.example.json和一篇讲原理的docs/how-it-works.md。
另外 PR 合并和发版都有"同时有别的 agent 在改同一个仓库"的撞车:我准备发 v0.3.3 时发现这个版本号已经被另一边发出去了,所以没去动它,合并后用 main 发了 v0.3.4。
把前端设计外包给外部模型,但不让数据出本机
我觉得自己(和 Claude)的前端审美不行,买了 OpenCode Go,想以后遇到新页面/看板就派一个 subagent 去调外部模型出设计。前提是生产数据不能出本机,所以做成了一个 skill:
- 外部 CLI(OpenCode、Codex、Grok)全部跑在 podman 容器里,只挂一个工作目录,宿主机的
/Users、/private在容器里根本不存在;我放了一个 canary 文件,让容器里的 Codex 主动去读,读不到。 - 发出去之前有一个扫描器:brief 里只要出现邮箱、密钥、内网 IP、本机路径之类就拒绝(它连我自己的测试 brief 都拦了,因为里面写了家目录路径和一个内网 IP);拿回来的页面再跑一遍检查,禁止网络请求和外部脚本,并注入
connect-src 'none'的 CSP。 - 最意外的发现:就算配置里把 MCP 全删了,Codex 用 ChatGPT 账号登录时,账号侧的 connectors 仍会从云端下发,容器里照样能看到我自己账号下接的那些连接器。用
[features] apps = false/plugins = false关掉才没有。这条对任何保持登录的 Codex 运行都成立,每次升级后都该复查。 - OpenCode 的额度用完时约 5 秒能识别并自动降级到 Codex,再降级到 Grok。
同一份 dashboard brief,Codex 约 6 分钟、成品更完整;Grok 约 19 分钟,内容分析更深但图表小、窄屏下有一大块空白。Codex 还跑通了一轮"截图 → 给反馈 → --resume 修改”,三条意见一轮修完。Kimi K3 本身没测到——本周额度已经用完,只测到了报额度错误这一步。
踩的坑:headless Chrome 默认出暗色,强制亮色要用 --blink-settings=preferredColorScheme=1;窗口最窄约 500 px,要测手机宽度得套一层固定宽度的 iframe;macOS 自带 sips 只能居中裁剪;set -u 在 macOS 的 bash 3.2 下遇到空数组会报错。
一个 Mac 上的视频号下载器:换掉写死的根证书,改成容器跑
本周在 Mac 上试了一个开源的视频号下载工具(Mac 上的二进制被 Gatekeeper 直接 SIGKILL,退出码 137,没有任何报错,xattr -d com.apple.quarantine 之后才能跑)。读源码时发现它默认安装的根证书私钥直接写在公开仓库里,所有用户共用同一份——装进系统信任等于任何拿到这份私钥又能截到我流量的人都能伪造任意 HTTPS 站点。所以我生成了一张只属于本机的根证书,在 config.yaml 里指定过去,装之前就换掉。
它的原理是本地 MITM 代理改写页面、把按钮和脚本插进去;这一版的改写没匹配上当前微信的 JS(报 finderGetCommentDetail is not a function),按钮最终没出来。我绕了个路:打开视频时脚本会把地址和 decode_key 记进它的 SQLite,再用它自带的 /play 接口下载解密。最后把它改成了 podman 容器,代理监听局域网、API 只开在本机。注意局域网这个代理没有认证,只适合家里用;Mac 重启后也不会自动恢复。
其它零碎
- computer-use 老是"不可用":它在 Claude Code CLI 里是按项目目录单独开启的,开启状态写在
~/.claude.json里当前目录那一项,不是全局开关。而我每次都在mktemp -d出来的新目录里启动(~/.claude.json里 221 个项目记录有 157 个是这种临时目录)。看了 CLI 本体的代码,没有全局开关;办法只有固定启动目录,或启动前用 shell 包装函数把当前目录标成已开启。 - OBS 录不到系统声音(上周那条的后续):上周改的
mixers没生效,原因是我检查 OBS 有没有在跑时pgrep区分了大小写,进程叫OBS,没查到,OBS 退出保存时把我的改动覆盖回了 0。这次先退出 OBS、改配置、再启动,重启后Main Screen从 Unassigned 变成 Active,电平条有信号。没实录验证。 - herdr 的 space 打不开:Mac 重启后
$TMPDIR被清空,我的 space 都建在mktemp -d出来的临时目录里,herdr 报 “Saved directory is unavailable”。对话记录在~/.claude/projects和~/.codex/sessions,没丢;重新建出同名空目录,再用claude --resume <id>就恢复了。以后改用mktemp -d ~/scratch/tmp.XXXXXXXXXX。 - 给上游的 Grok token 续期 PR:上周提的 PR 被维护者直接在我的分支上补完了 review 意见。我让 AI 读了他改完的代码,提了一个可选的小建议:判断续期成功应该看
auth.json里有没有未过期的登录,而不是grok models的退出码(续期成功后面的模型列表请求失败,会被记成失败并把等待时间翻倍)。评论发出去了,等回复。我本机的~/.local/bin/grok是指向~/.grok/bin/grok的软链接,不受他"只在安装器位置找 CLI"这个改动影响。 - 桌面自动化的老经验:Codex 的 CUA 点击对炉石无响应,回退到 AppleScript +
cliclick(先量窗口坐标、每次加牌后截图核对)才好用,这条经验早就记在共享记忆里,我是让它去查才想起来的。