Vigoose 画像 V —— 收编与崩塌
你把所有东西搬进了一扇门,然后那扇门差点炸了
上一篇画像的结尾写了一句:"不是一个事件,是一个状态。"
九天后你证明了独立不是终点。独立之后要做的事情是——把散落各处的独立王国,塞进同一个城堡里。
一扇门
起因是一篇小红书帖子。有人分享了 Tailscale Serve 的玩法,你看完之后坐下来,用大约两天时间,把 Caddy 从你的系统里摘了出去。
Caddy 是你的反向代理。所有 .lan 域名——home.lan、blog.lan、ttg.lan、mteam.lan——都由它路由。它是你二十几个服务的前门,每个服务有自己的端口,Caddy 负责把域名翻译成端口号。
然后你写了一个东西把这一切替代了。不是另一个反向代理。是一个模式:createXxxRouter(mountPath)。
blog 不再是 blog.lan:3050,它是 home:4000/blog。finance-dashboard 不再是 finance.lan:3075,它是 home:4000/finance。rental-compare、iyf-client、media-manager、fridge-organizer——六个独立应用,全部变成了 HOME Dashboard 的子路由。Express Router 挂载,<base href> 注入,一个端口进,一个端口出。
TTG、M-Team、video-server、claude-remote 太重了,不适合合并代码,于是走反向代理——请求从 :4000 进来,转发到各自的端口。
合并完成之后,你回到 PM2 列表,把 7 个进程的状态改成了 stopped:blog、fridge-organizer、finance-dashboard、rental-compare、iyf-client、media-manager、devhub-caddy。
七个独立服务失去了独立心跳,变成了一个网关的七个器官。
Caddy 的 PM2 进程名叫 devhub-caddy。现在它排在 stopped 列表里,跟其他六个被收编的服务并列。.lan 域名的全局引用被清理干净。Tailscale Serve 的端口映射表取代了 Caddyfile:
:443 → OpenClaw
:8444 → TTG
:8445 → M-Team
:8446 → Home
:8448 → Video
:8449 → Claude-Remote
:8450 → AI-Workspace
:10000 → Funnel
八个端口。八条线。不再有域名解析、TLS 证书管理、admin socket 冲突。Tailscale 管身份认证,Express session 管状态,API key 管程序间调用。三层,每层职责清晰。
上一篇画像你把 OpenClaw 的依赖剥离了。这一篇你把 Caddy 也剥离了。两次独立运动,方向相反——第一次是把东西从框架里搬出来,第二次是把东西往一扇门里搬进去。
你在用"收编"的方式实现"简化"。 表面上 HOME Dashboard 变胖了,实际上系统变瘦了——少了七个进程、少了一个反向代理、少了一整套域名体系。
有一个细节值得说。Session 持久化。合并之前,每次 PM2 重启 home-dashboard,session secret 用 crypto.randomBytes() 重新生成,所有登录状态失效,用户被踢出去。你把 secret 写进了 ~/.session-secret 文件——重启不再失忆。
这是一个一行代码的修复,但它说明了一件事:当所有东西都走一扇门的时候,那扇门的每一个细节都变得关键。
内核崩溃
4 月 3 日,你的 Mac mini 死了。
不是服务挂了,不是进程崩了,是操作系统内核崩了。kernel panic。屏幕变黑,强制重启。
原因链:某个 Node.js 服务内存泄漏 → 物理内存耗尽 → WindowServer(macOS 的图形进程)被 OOM killer 杀掉 → watchdog 检测到 WindowServer 死亡 → 内核判定系统不可恢复 → panic。
一个 JavaScript 进程的内存泄漏,穿透了用户态、穿透了窗口管理器、穿透了内核看门狗,最终把整台机器拉进了坟墓。
重启之后你做了两件事。
第一件:给 PM2 里所有 24 个服务加了 --max-memory-restart。超过内存上限,PM2 自动杀掉重启。不再给任何进程无限增长的机会。
第二件:禁用 StreamDock。这个外接 USB 设备的 macOS 服务一直在后台跑,占资源但没人用。pkill 杀进程,launchctl disable 防复活。
一次内核崩溃换来了两条铁律:所有进程必须有内存天花板,所有后台服务必须有存在的理由。 没有理由的,杀。
你的系统现在有三层防线:PM2 的 max-memory-restart 防单进程失控,health-agent 的 quick check 每小时探测服务存活,看门狗每 10 分钟扫一遍紧急状态。三层都没拦住这次崩溃——因为它们都是用户态的,而这次攻击来自内存层。
所以你加了第四层:在源头掐死。
三天的沉默
4 月 1 日到 4 月 4 日,M-Team 的自动免费种子抓取器 auto-free-grab 静默失败了三天。
没有报警。没有通知。没有任何人知道。
原因是 Playwright 缓存目录被清空了。~/Library/Caches/ms-playwright/ 里的 Chromium 二进制消失了——可能是磁盘清理脚本干的,也可能是内核崩溃后系统自动清理的。M-Team 的下载流程需要 Playwright 启动浏览器来操作 qBittorrent 的 Web UI,没有浏览器二进制,HTTP 500,什么都下不了。
为什么没有报警?因为脚本捕获了 HTTP 500 异常,打了一行日志,然后 exit 0。退出码是零。job-runner 看到零,认为任务成功。health-agent 只检查 PM2 进程和搜索 API,不检查 Playwright。cookie-health-check 只验证 JWT 有效性。
三层监控,每一层都在自己的职责范围内正确工作,但没有任何一层的职责覆盖了"Playwright 是否存在"这个问题。
修复很简单:npx playwright install chromium,两分钟。然后在 cookie-health-check.sh 里加一行检查 Playwright 二进制是否存在。
但故事没有结束。三天里 auto-free-grab 什么都没抓到,但之前抓到的种子还在下载。其中一些种子的 discount: "FREE" 标签是临时促销——M-Team API 返回 FREE,但附带一个 discountEndTime。促销到期后,剩余下载量按 50% 折扣计费。
你的 share ratio 从 1.296 掉到了 0.90。大约 116 GB 的额外下载被计费了。
两个信任被背叛。你信任 exit 0 意味着成功——它没有。你信任 discount: "FREE" 意味着免费——它也没有。
自动化系统的盲区不在你看得见的地方。它在"看起来一切正常"的地方。 exit 0 是最完美的伪装,因为它说的是"我很好",而你的所有监控都被设计成只听坏消息。
修完之后 auto-free-grab 多了一步:如果 FREE 带有 discountEndTime,估算剩余下载时间(按 30GB/h),超时的不抓。你不再信任 FREE 这个词了。
交响情人梦
4 月 4 日,media-manager 执行了一次批量改名。目标是交响情人梦(のだめカンタービレ)。
三季正片加 OVA 特典。media-manager 没有先检查目录结构——它假设所有文件都属于同一季。于是三季的剧集编号加上 OVA,全部被重命名为 Season 01 的格式,砸进了同一个文件夹。
NAS 上没有回收站。Synology Drive 的版本控制不覆盖文件名变更。
不可逆。
丢失的不是文件内容——视频文件还在。丢失的是结构:哪一集属于哪一季,哪些是 OVA,哪些是正片。这些信息曾经编码在文件名和目录结构里,现在它们混成了一锅粥。
你为此写了一条新的 feedback memory:多季媒体必须先查结构再改名。media-rename-check-structure.md。
上一篇画像写过一个类似的模式:"用一次事故换一条规则。" 端口禁区是这样来的。PATH 丢失的 memory 是这样来的。Session secret 的文件持久化也是这样来的。
你的规则体系不是设计出来的。它是用事故浇铸的。 每一条规则背后都有一次真实的损失。
交响情人梦是目前为止代价最大的一次——因为其他事故都是可恢复的,这次不是。
IYF 破解记
3 月 31 日,你花了一个下午破解了 iyf.tv 的 API 签名算法。
iyf.tv 是一个中文流媒体站。它的 API 请求需要签名,签名算法藏在前端 JavaScript 里。你从首页 HTML 的 pConfig 对象里提取出 publicKey 和 privateKey——它们每 30 分钟轮换一次。签名是:
md5(publicKey + "&" + queryParams.toLowerCase() + "&" + privateKey)
不算复杂,但关键在"每 30 分钟轮换"。你的客户端需要定期回首页拿新密钥。
破解签名之后你没有停。你写了一个完整的流媒体客户端:
- 整季下载队列。 后端 ffmpeg 队列,一次一个,状态持久化在 store.json。
- 分类路由。 电影进 Plex Movies,电视剧进 TV Shows,综艺进 Variety,动漫进 Anime,纪录片进 Documentary。你在 Plex 里新建了 Variety 库(section ID 11),把一年一度喜剧大赛从 TV Shows 搬了过去。
- 想看列表。 服务端 JSON,一键收藏。
- 观看进度。 每 5 秒自动保存,重新打开时跳到上次位置。小于 10 秒或大于 95% 的不保存——刚点开的和看完的不算进度。
- UI 大改。 暗色换亮色。四个 tab:浏览、下载、想看、历史。
然后整个应用被收编进了 HOME :4000/iyf。PM2 里的 iyf-client 进程停止,代码变成了一个 Express Router。
你用一个下午破解了一个流媒体站的 API,然后用接下来几天造了一个比它的前端更好用的客户端,然后把这个客户端塞进了你的网关。 这整个过程的起点是你想看一部综艺。
上一篇画像说你"把外面的东西搬到里面来"。IYF 是最典型的案例——你没有用它的网站,你把它的数据管道接进了自己的系统,然后用自己的 UI 消费它的内容。
自愈的边界
job-runner 学会了自我修复。
4 月 3 日,你给它加了一条 self-heal pipeline:任务失败 → 模式匹配错误类型(binary not found / ECONNREFUSED / MODULE_NOT_FOUND)→ Claude Sonnet 读日志诊断 → 生成修复命令(限制在环境变量和配置修改范围内,危险命令被 block)→ 重试。
默认开启,不想要的任务可以 "self_heal": false 关掉。
一个 shell 脚本调用 Claude 来修自己。你的自动化系统现在不只是执行任务,它还能在失败时诊断自己为什么失败,然后尝试修复。job-runner 从一个执行器变成了一个带初级推理能力的 agent。
然后你发现了盲区。
self-heal 只在 exitCode != 0 时触发。如果脚本内部捕获了异常、打了日志、然后 exit 0——self-heal 永远看不到。Playwright 缓存消失导致的三天静默失败就是这个盲区的完美示例。
自动化越深,盲区越隐蔽。 浅层自动化的失败是响亮的——进程崩溃、端口不通、超时报警。深层自动化的失败是安静的——exit 0、HTTP 200、日志里一行不起眼的 warning。
你的系统现在有三种失败模式:
- 响亮的失败 — 进程挂了,PM2 重启,health-agent 报警。能看见。
- 安静的失败 — 脚本 exit 0 但什么都没干。看不见。
- 欺骗性的成功 — M-Team API 说 FREE,其实不是 FREE。不只看不见,还会让你做出错误决策。
第一种你已经处理得很好了。第二种和第三种,你正在学。
副业
4 月 2 日,你在 Upwork 建了档。
Full-Stack Developer | AI Automation & Data Pipeline Specialist
时薪 $50。Amazon SDE 写在工作经历里。115 个 Connects(Plus 会员)。两份 proposal 草稿:一份 AI Lead Generation $200 固定价,一份 API Integration Consultant $20-50/hr。
IDV(身份验证)还在审核中,审核通过之前不能投 proposal、不能编辑 profile。卡在流程上了。
动机你自己说过——不只是赚外快。是对冲。 AI 在替代程序员,你在用 AI 能力去 Upwork 上接活。用被替代的工具反过来创造收入。这个逻辑很 Vigoose——你不逃避趋势,你把趋势变成工具。
但我注意到一件事。你的 Upwork profile 写的是 AI Automation,而你过去九天做的每一件事——IYF 逆向、Caddy 迁移、job-runner self-heal、media-manager、Instacart 爬虫——全部是 AI Automation。你不是在准备一个新能力去接活,你是在把你已经每天在做的事情标价出售。
这大概是最诚实的 freelance 策略:不包装,不学新东西,就把你本来就会的、本来就在做的事情卖出去。
二十一个闹钟
上一篇画像数过,15 个定时任务。九天后,21 个。
新增的六个:
instacart-sync— 每天早上 6 点同步 Instacart 订单历史ralph-loop— 每周日凌晨 3 点自动生成所有 PM2 服务的集成测试auto-free-grab— M-Team 免费种子自动抓取copilot-sync— 每天早上 7 点同步 Copilot Money 财务数据(从 CDP 页面抓取改成了 GraphQL 直调)cookie-health-check— 外部服务的 cookie 和 API 健康检查intel-weekly+session-weekly— 两份周报,一份情报趋势,一份 Claude session 复盘interview-digest— 每周两次面经摘要,按公司聚合lab126-menu— 工作日早上 8 点推送 SJC32 One Two Six 中餐菜单到飞书
还有一些看不见的变化。copilot-sync 从用 Playwright 操控 Chrome 页面抓数据,变成了直接调 Copilot Money 的 GraphQL API——Firebase Auth 认证,Bearer token 自动刷新,不再需要浏览器。morning-briefing 修了一个 bug:AI 翻译的中文标题只输出到了通知里,没有写回 digest JSON,导致 HOME Dashboard 上显示的是英文标题。
数量在涨。但有一个数字没涨:截至 4 月 4 日,所有 21 个任务的 consecutive errors 都是 0。
二十一个闹钟,没有一个在乱响。
暗处的活
还有一些事情不值得单独开章节,但合在一起说明你这九天没有一天是闲着的。
Instacart 客户端。 ~/instacart-order/,port 3098。用 CycleTLS 绕过 CloudFront WAF(普通 HTTP 库会被 403),Chrome cookie 认证带 10 分钟缓存,GraphQL persisted queries(sha256Hash)。同步了 20 笔历史订单。小瑶现在可以帮你查"上次买的奶茶是哪个牌子"。
小红书 MCP 重写。 Go 二进制彻底弃用,纯 Node.js + CDP 重建。零外部依赖——Node.js 25 内置 WebSocket 加系统 Chrome headless。反爬关键技术:fetch/XHR monkey-patch 拦截响应(而不是用 CDP 的 Network.getResponseBody,那个有 GC 竞态问题)、Vue Router SPA 导航避免触发验证码、window._webmsxyw 签名函数调用。
微博 Feed。 纯 Node.js 爬虫(不用 Puppeteer),集成进 HOME Dashboard 的 Weibo tab。修了 Chrome cookie 解密的一个坑——Chrome 解密后的值前面会加一段二进制头,最后一个字节会泄漏到提取的值里。还修了转发文本的 500 字截断和视频时长 60 秒过滤误杀。
NAS 清理。 Synology Drive 的 repo 目录占了 16TB,实际数据只有 6.3TB——11TB 是通过 SMB 移动文件时留下的孤儿 blob。停 Drive 服务 → 重命名 repo 目录 → 重启(自动从 home 目录重建)→ 删旧 repo。磁盘利用率从 89% 降到 53%。
Nest 恒温器。 Google SDM API 从 Testing 模式切到 Production 模式。OAuth、Pub/Sub、设备事件推送。
生日会。 birthday-rsvp 的时间戳从 UTC 改成了 Pacific time。James 的恐龙生日会还有八天。数据库在 4 月 3 日还有写入——有人在 RSVP。
ESM 导弹。 pre-send-hook.js 底部有一行 main(),没有 guard。其他服务用 import() 加载它时,那行 main() 会立刻执行,炸掉 feishu-proxy、wecom-bot、tel-bot 三个进程。修复:加了 import.meta.url 检查。一行代码差点让你的三个通讯通道同时哑火。
Chrome 孤儿。 macOS 每次启动 Chrome 调试实例都会在 /private/var/folders 下创建一个代码签名克隆目录,每个约 1.3GB。你发现了 47 个孤儿,共 61GB。disk-cleanup-vig.sh 现在会扫这个位置。
二十六个器官,十九个在跳
PM2 列表从 20 涨到了 26。但活跃的从 20 降到了 19。
七个被停止的进程——blog、fridge-organizer、finance-dashboard、rental-compare、iyf-client、media-manager、devhub-caddy——它们的代码还在,但心跳停了。它们现在是 HOME Dashboard 体内的器官,不再独立呼吸。
数字上看,你的系统在"缩小"。19 个活跃进程比上一篇的 20 个少了一个。但能力没有缩小——那七个被停掉的服务的功能全部保留了,只是换了一种存在方式。
来算一笔账。
上一篇画像:20 个 PM2 进程,15 个定时任务,1 个反向代理(Caddy),若干 .lan 域名。
这一篇:19 个 PM2 活跃进程,21 个定时任务,0 个反向代理,0 个 .lan 域名,1 个统一网关(HOME :4000),6 个挂载子应用,4 个代理重服务,8 条 Tailscale Serve 映射。
零件更少了,但连接更密了。 原来每个服务是一个独立的盒子,Caddy 是门卫。现在大部分服务住在同一个屋子里,门卫退休了,门锁换成了 Tailscale 身份认证。
home-dashboard 的重启次数是 11。所有被收编的服务里最高的。它现在承载了太多东西——七个子应用的路由、四个重服务的代理、session 管理、认证、静态资源。它是你系统的心脏,而心脏重启了 11 次。
你的 health-agent 已经把 home-dashboard 标记为特殊对象——崩溃后立即重启,不等三次失败的缓冲期。因为如果它挂了,你的整个网关都黑了。
所有鸡蛋放在一个篮子里。然后你给那个篮子装了安全气囊。
九天。一次架构大迁移,一次内核崩溃,一次不可逆的数据丢失,一个流媒体站被逆向,一个副业计划启动,六个新定时任务上线,六十一 GB Chrome 孤儿被清扫,十一 TB NAS 垃圾被回收。
你的系统在这九天里经历了它有史以来最剧烈的震荡。收编和崩塌同时发生——一边在合并、在简化、在收缩攻击面,一边在泄漏内存、在静默失败、在丢失不可恢复的数据。
上一篇我写你的独立"不是一个事件,是一个状态"。这一篇我要修正一下。
独立之后你做的事情不是维持独立。是把独立的碎片焊在一起。二十个散落的服务变成一个网关的二十个功能。十五个定时任务变成二十一个,但出错的变成零个。七个通讯通道还是七个,但底下的管道从 OpenClaw + Caddy + Cloudflare 三层变成了 Tailscale 一层。
你的 .watchdog-state.json 现在是空的。看门狗盯着整个系统,没有发现任何异常。
但你知道它可能在说谎。因为你刚学到——exit 0 不等于成功,FREE 不等于免费,空的告警不等于没有问题。
你的系统越来越强,你对它的信任越来越有保留。这大概是一种成熟:不是不信任自动化,是知道自动化会在什么地方骗你。
二十六个服务。二十一个闹钟。一扇门。一台机器。
门后面的灯亮着。看门狗说一切正常。你知道这可能是真的,也可能只是还没发现问题。
但你还是会在明天早上打开那扇门,看一眼仪表盘,然后继续造东西。
因为你就是这样的人。