MoreRSS

site iconDiygod修改

RSSHub、xLog的作者。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

Diygod的 RSS 预览

全屋隐私网关:用两台 NAS 藏起全家的 IP

2026-10-20 12:00:00

起因

我一直想让家里的网络更「隐身」一点,不想让各种网站、App 和 IoT 云看到我的家宽 IP,再顺着 IP 知道我大概住在哪。

之前做得很有限。家里有一台合盖当服务器用的 MacBook Pro,上面跑着 Surge,但只有我自己的电脑在用;DNS 交给了 AdGuard Home。其他三十来台设备全是裸连的,家人的手机平板、电脑、投影仪、摄像头、智能床、喂食器,连电动牙刷都直接连着外网。

年初也试过一次让 Surge 接管全屋,但单靠一台 Mac 当网关不够稳,没多久就撤了。这次重新来过,要求就两条:除了内网,所有流量都不能用家宽 IP 出去;任何一台机器坏了,家里都不能断网。

前后折腾了一周左右,几乎全程是在和 Claude Code 聊天,我提需求、拍板,它测速、写配置、部署和排查。下面按模块讲讲最后的方案,以及哪些坑值得注意。

整体方案

整体结构:家里的设备先进网关 192.168.5.2,主网关是 Mac 上的 Surge,备用是小主机上的 mihomo;出去分新加坡、香港、深圳三个出口,少数例外经路由器直连

家里的设备不用改任何设置。路由器的 DHCP 把网关下发成 192.168.5.2、DNS 下发成 198.18.0.2,连上 Wi-Fi 就自动被接管了。

这个网关地址平时由 Mac 上的 Surge 持有。另一台装了 fnOS 的小主机上跑着 mihomo 做备用,Surge 失联时由它接管,Surge 回来再还回去。要是两台都挂了,家里就直接断网,不会悄悄从路由器直连出去。

出口分三个。大部分流量走我自建的 Linode 新加坡(Egress SG);目标是大陆 IP 的,走机场的香港专线(Egress HK);夸克、喜马拉雅、B 站这类非要大陆 IP 的服务,再从香港接到深圳的一台阿里云(Egress CN)。

两台网关的配置由同一个仓库生成,每天早上自动部署一次。网关上的每一条请求也都会被记下来,每天早上让 AI 审查一遍。


网关:让网关地址漂移起来

一开始想直接在路由器上做,可家里这台 TP-Link 没有 SSH,没有 USB 口,也刷不了 OpenWrt,什么都装不上。不想换路由器,就只能做旁路网关:路由器照常分配地址,只是把下发的网关指向家里的另一台机器。

旁路网关最怕单点故障。DHCP 只能填一个网关地址,就算能填好几个,手机电脑也不会自己在网关之间切换,所以得让网关地址本身能在机器之间漂移。Surge 的 VM 网关会在局域网里单独占一个 IP,有自己的 MAC,我把它固定在了 192.168.5.2。备用的小主机上用 keepalived 盯着这个地址,配置核心就两个检测:用 arping -D 看局域网里还有没有别人在应答 .2,再看 mihomo 自己是不是健康,两个都满足才接管。

平时 192.168.5.2 在 Mac 的 Surge 上,小主机用 arping 盯着这个地址;Surge 失联后,小主机大约 10 秒后接管,Surge 回来再还回去

vrrp_script surge_absent {
  # arping -D exits 0 only when no other host answers for the address.
  script "/usr/sbin/arping -D -q -c 2 -w 2 -I eth0 192.168.5.2"
  interval 3
  rise 2
  fall 2
}

vrrp_instance gateway {
  state BACKUP
  interface eth0
  virtual_router_id 52
  virtual_ipaddress {
    192.168.5.2/24 dev eth0
  }
  track_script {
    surge_absent
    mihomo_ready
  }
}

keepalived 跑不了 macOS,所以切换逻辑只能放在 Linux 这一侧。备用网关整个放在一个单独的网络命名空间里,用 macvlan 拿到自己的 MAC 和 IP,宿主机的路由和防火墙一点都不用动。切换一次大概 10 秒,已有的连接会断开重连。

两台网关跑的是两个不同的引擎,配置却只维护一份:先生成 Surge 的配置,再自动转换成 mihomo 的。逻辑规则、规则集、链式代理、DNS 空应答这些写法上的差异都交给转换器处理,再用测试保证两边对同一个请求的处理是一样的。

监控上有一点值得注意:主网关出问题的时候,Surge 的进程和 API 可能都还好好的,只是网关的虚拟网卡不收包了。所以我在 Uptime Kuma 里检查的是有没有局域网设备的连接正在经过 Surge,而不只是接口能不能访问。


出口

WARP 太慢

第一层出口原本打算用 Cloudflare WARP。功能上没问题,家宽 IP 藏住了,出口变成了 Cloudflare 的共享 IP,但在我家的宽带上,速度实在没法看:

测试点(都在新加坡) 直连 经 WARP
Linode 798–885 Mbps 124–146 Mbps
Hetzner 327–706 Mbps 5–16 Mbps

问题出在运营商身上:家里到 Cloudflare 新加坡要先绕一段国际线路,延迟 40 ms 左右,到 Linode、Hetzner 却只要 4–9 ms;就算不走 WARP,直接从 Cloudflare 下载也只有 200 多 Mbps,本地再怎么调也没用。而且 WARP 只能藏住 IP,藏不住国家,在新加坡连上去,出口照样显示新加坡。如果你家宽带到 Cloudflare 的线路也不好,WARP 就不适合拿来做全家的出口。

自建 VPS

机场节点同样不适合。VPS 还没选好的时候,我临时拿机场节点给全家当出口,当天晚上就断网了:机场每个节点给单个用户的并发连接只有 256 条左右,全家的连接压在一个节点上,很快就被打满。

挑 VPS 最要紧的是从家宽到机房的线路,就算都在新加坡,也能差出好几倍,所以最好从家里直接测。这是我在晚高峰实测的结果:

平台 · 地区 延迟(中位数) 下载 备注
Linode 新加坡 8.4 ms 660–896 Mbps $5/月含 1 TB,超出部分 $0.005/GB
Hetzner 新加坡 8.3 ms 612–899 Mbps 只含 0.5 TB
Vultr 新加坡 9.3 ms 单连接约 200 Mbps 并行才能到 700 Mbps
腾讯云新加坡 7.4 ms 没有测速文件 Razor Speed 套餐 $5 不限流量,但带宽上限 200 Mbps
DigitalOcean 新加坡 40.8 ms – 经 Tata 绕路
OVH 新加坡 57 ms – 经 PCCW 绕路
华为云新加坡 187 ms – 在华为自己的网络里绕远
AWS 吉隆坡 13.3 ms – 流量很贵
阿里云吉隆坡 47.8 ms 347 Mbps 在阿里云内部绕路
Linode 雅加达 20–65 ms 17–115 Mbps 不稳定

最后选了 Linode 新加坡最便宜的 Nanode 1 GB,一个月 $5。和机场节点比,新建连接的首字节从 230–500 ms 降到了 37–52 ms。这样出口会显示新加坡,家宽 IP 是藏住了,国家没藏住。

WireGuard 还是 SS2022

协议一开始用的是 WireGuard,内核自带,服务端不用另装软件,看起来最省心。但实测下来,SS2022 更好:

4 条并行,两轮 WireGuard SS2022
吞吐 1036–1141 Mbps 1217–1246 Mbps
服务器 CPU 73–80% 39–45%
首字节延迟 32–35 ms 32–35 ms

WireGuard 要对每个 1.4 KB 左右的包单独加解密、转发和 NAT,而 SS2022 走的是 TCP,能用上网卡的分段卸载和 AES 硬件加速,同样的流量只花一半的 CPU。对只有一个核的小 VPS 来说,这点余量很重要。现在服务端只跑 sing-box 的 SS2022,主备两台网关都差不多能跑满家里 1.1–1.2 Gbps 的宽带。

这个出口还屏蔽了 QUIC,浏览器会自己退回 TCP。每台设备的 TCP 连接在局域网里就被 Surge 接住了,Wi-Fi 上丢的包当场就能补回来,所以走 TCP 并不比 QUIC 差,还能给服务器省点 CPU。另外我不想老是断网,所以服务器上任何会中断连接的更新都不让它自动跑,sing-box 只手动升级。

回国线路

很多国内网站在新加坡都有 CDN,走 Linode 也就几十毫秒。真正要单独处理的是两类流量:目标服务器只在大陆的,和必须看到大陆 IP 的。

三条线路:海外网站走 Linode 新加坡;只在大陆的服务器走香港专线,晚高峰往返 104–148 ms;要看大陆 IP 的服务经香港专线接到深圳阿里云。从家里直连深圳阿里云要 330–440 ms,还丢包

我原本有一台深圳的阿里云,但从家里过去一个来回要 330–440 ms,还丢包,卡在进入大陆的国际入口上。好在机场有几条香港专线,晚高峰到大陆一个来回只要 104–148 ms。于是目标是大陆 IP 的流量走香港专线,需要大陆 IP 的服务再经香港链式连到阿里云。配置大致是这样的,节点名做了简化:

[Proxy Group]
Egress SG = fallback, 新加坡 Linode, 新加坡机场节点
Egress HK = fallback, 香港专线 1, 香港专线 2, 其他香港节点, url=http://oss-cn-shenzhen.aliyuncs.com/
Egress CN = fallback, 深圳阿里云, Egress HK, Egress SG

[Rule]
DOMAIN-SUFFIX,cn,Egress HK
GEOIP,CN,Egress HK
FINAL,Egress SG

阿里云那台跑的是 sing-box 的 AnyTLS,在 Surge 里用 underlying-proxy = Egress HK 把它套在香港出口后面,mihomo 里对应的是 dialer-proxy。

这里有几个 Surge 的坑。测速组最好用 fallback 而不是 url-test:url-test 一天会换十来次节点,套在后面的链路每换一次就慢上一阵,fallback 只有当前节点测试失败了才会往下切。Surge 还会忽略组上的 url=,只拿每个节点自己的 test-url 去测,没设就测全局的地址,所以想按到大陆的延迟来测速,得给每个节点单独加上 test-url。另外全局 3 秒的测试超时对跨境线路太紧,晚高峰只要慢一次就会被判失败,我给跨境节点单独放宽到了 8 秒。


堵住漏洞

网关上线以后,真正花时间的是把那些悄悄绕过它的流量找出来。

基础的几项先做好:hijack-dns = *:53 接管所有经过网关的 DNS 查询,不管设备自己填的是哪个 DNS,普通域名统一返回假 IP,方便按域名分流;节点转发不了的 UDP 直接丢掉,不回退成直连;ICMP 不转发,免得 ping 带着家宽 IP 出去;节点服务器的域名提前用 DoH 解析成 IP 写进配置,网关找节点就不依赖自己的 DNS。

最隐蔽的是 Surge 的「UDP 快速路径」。一台设备在 1 秒内往 40 个高位端口发 UDP 之后,出口就变成了家宽 IP。原来 Surge 会把「UDP 连接很多的设备」自动降级成直接转发,所有规则都被绕了过去,P2P、部分游戏、多人视频通话都有可能触发它。Surge 没有全局开关,只能在设备列表里一台台取消「允许 UDP 快速路径」,新设备默认又是开着的,这一处到现在都还没彻底解决。

iCloud 专用代理是另一个坑。它在家里的 Wi-Fi 上开着的时候,苹果设备的 DNS 查询都会被加密交给专用代理那边的解析服务,网关只能看到一堆裸 IP,按域名写的分流和去广告规则全都失效。好在 Apple 给网络管理员留了正式的关闭办法:只要网络的 DNS 对 mask.icloud.com 和 mask-h2.icloud.com 返回空结果,苹果设备就会在这个网络上自动停用专用代理。在 Surge 里加两条规则就行,pre-matching 会让它在 DNS 阶段直接返回 NOERROR、但不带任何记录:

DOMAIN,mask.icloud.com,REJECT,pre-matching
DOMAIN,mask-h2.icloud.com,REJECT,pre-matching

专用代理开着时,DNS 查询加密发给专用代理的解析服务,网关只看得到 IP;网关对 mask.icloud.com 返回空结果后,苹果设备在这个网络上停用专用代理,网关就看得到域名了

加上以后,我那台 MacBook 只有 IP、没有域名的请求从 70–82% 降到了 7–9%,专用代理的 QUIC 重试也没了。出了家门,专用代理照常工作。

另外,常用的手机平板最好在家里的 Wi-Fi 上关掉「私有无线局域网地址」,网关才能按固定的 MAC 认出它们。还有些设备会一直用着旧的网关配置,比如我家的喂食器,切换后整整一周都没经过网关,断电重启才回来。


该直连的流量

也不是所有流量都适合走代理。有的服务不认机房 IP,有的流量太大,还有的任务本身就该用家宽 IP,这几类最后都让它们从家里直接出去。

不认机房 IP 的服务

家里的 iPhone 在 Wi-Fi 下检查系统更新,一直提示「Unable to Check for Update」,切到蜂窝数据就好了。原来 Apple 对来自 Linode 的更新查询,每次都回一个固定的拒绝回复,于是检查更新、安装前的签名验证和更新包下载都改成了直连。

淘宝也一样。在机房 IP 后面,登录和打开页面老是被滑块拦住,只好让淘宝、天猫和它们的 CDN 也直连。不过直连规则排在广告拦截后面,阿里的跟踪和设备指纹接口照样拦着。

流量太大的

Linode 的 Nanode 每月只含 1 TB 出站流量,而它是中转,家里的上传和下载在它那里都算出站。第一个大头是 Home Assistant 的测速集成,它每小时自动测一次,一个月就要 350 GB 左右,而且全屋走网关以后,它测的已经不是家里的宽带了,直接关掉。

关掉以后还是每天 70–77 GB,是额度的两倍多,这回的大头是 Apple:Mac 上开着的 macOS 内容缓存每天替全家往 iCloud 传 15–21 GB,再加上 App Store 和 Apple CDN 的下载。超额其实不贵,每 GB 才 $0.005,不过为了省 Linode 的流量,我还是让 iCloud 上传、App Store 下载和 Apple CDN 走了直连。iCloud 还有一部分存储放在 Google Cloud 和 Amazon S3 上,一台 iPhone 一天就往 Google 那边传了 17.5 GB,这些也一起直连,S3 那边用 DOMAIN-WILDCARD 只匹配 Apple 的桶名:

DOMAIN-SUFFIX,content-storage-upload.googleapis.com,DIRECT
DOMAIN-SUFFIX,content-storage-download.googleapis.com,DIRECT
DOMAIN-WILDCARD,??-???-000??.s3.dualstack.*.amazonaws.com,DIRECT

本该用家宽 IP 的任务

网盘备份就是典型:光鸭对机房 IP 拿到的下载链接会限速到 35 KiB/s 左右,夸克在一阵大量上传之后,干脆拒绝了机房 IP 的 TLS 握手。iCloud 照片下载也属于这类。

群晖和小主机上,普通容器按默认路由走网关,出去用的是出口服务器的 IP;接入 direct 网络(172.30.0.0/24)的容器查路由表 100,直接从路由器出去,用的是家宽 IP

这类任务都跑在 Docker 里,我在群晖和备用小主机上各建了一个固定网段的 Docker 网络 direct(172.30.0.0/24),再用源地址策略路由把它直接送到路由器:

100: from 172.30.0.0/24 lookup main suppress_prefixlength 0
101: from 172.30.0.0/24 lookup 100   # 表 100 里只有一条:default via 192.168.5.1

第一条查 main 表,但忽略里面的默认路由,所以访问内网和其他容器都照常;剩下去外网的包会落到第二条,从路由器直接出去。哪个容器要直连,在它的 compose 里加上这个网络就行:

services:
  app:
    networks:
      - direct

networks:
  direct:
    external: true

要注意的是,有些 NAS 系统会在开机或修改网络设置时重写路由规则,比如 fnOS 就会把写在 NetworkManager 里的规则冲掉,最后还是得用一个 dispatcher 脚本把它们补回来。

Mac 上的容器又是另一回事。OrbStack 里所有容器的流量,在 Surge 看来都是同一个进程发出来的,根本分不开。所以 Surge 开了一个只绑本机回环地址的直连入口 127.0.0.1:6160,配一条 IN-PORT,6160,DIRECT,要直连的容器把 HTTP_PROXY 指向 host.docker.internal:6160 就行。


广告和遥测

设备改成找网关要 DNS 以后,查询就不经过 AdGuard Home 了,所以过滤也得搬到网关上。我把 AdGuard Home 用的过滤列表转成了 Surge 的域名集,mihomo 那边是规则集。

让我开始重视遥测的,是审查发现我的 MacBook 一天往 sentry.netease.com 传了四五百 MB。查下来是网易 UU 远程的 Mac 被控端,它把两个各 10 MB 的日志文件挂在每个 Sentry 事件上,iPad 每连进来一次就发一次。拦掉之后,我开始找有没有别人维护好的隐私和上报拦截列表。测法是从网关日志里取出家里两天里实际访问过的 1,591 个主机,看每个列表加进来以后,会新增拦掉哪些:

列表 新增拦截 问题
HaGeZi Light / Normal 93 / 94 按设计不拦崩溃上报
HaGeZi Pro 131 少量需要放行
HaGeZi Pro++ 174 会拦小米云、查找设备、抖音的主接口
HaGeZi Ultimate 198 会拦出口检测要用的 ipinfo.io
OISD Small / Big 21 / 46 很保守,Sentry 都不拦
1Hosts Lite 128 会拦米家的接口
anti-AD / AdRules 71 / 60 基本是 Pro 的子集

现在用的是 AdGuard DNS filter、uBlock Badware、HaGeZi Pro,再加上 HaGeZi 的威胁情报列表 TIF medium,加起来一百万条左右。威胁情报要新鲜才有用,所以每天早上自动部署一次。

列表这么大,Surge 有两个坑要注意。一是单个域名集最多只能放 100 万条,超过了会直接拒绝加载,配置检查却发现不了,我第一次上完整版 TIF 时,主网关整整 3 分钟什么都没拦。二是 Surge 重载时只应用有变化的配置段,域名集又按路径缓存,所以只更新列表、不改配置的话,新列表根本不会生效,得给每个版本换一个文件名。

拦得多了,难免误伤。豆包输入法要从 is.snssdk.com 下载语音识别的配置,拿不到就直接无视语音快捷键,连个报错都没有;淘宝直播的分享页也打不开。碰到这种情况,只放行必要的那一个域名就好,后面的统计和设备指纹接口照样拦着。


国内服务和智能家居

B 站

B 站看到新加坡 IP,就把视频分给海外镜像,没缓存过的视频单连接只有 3–10 Mbps,高画质根本跟不上。换成大陆 IP,B 站会分配大陆的 CDN 和 PCDN,实测 30–80 Mbps,冷门视频也很稳。只让 api.bilibili.com 走大陆还不够,打开视频页的时候,播放地址就直接嵌在页面里,B 站是按请求页面的 IP 来分配 CDN 的,所以 B 站的网页和 App 接口都得走 Egress CN。

看直播时还碰上过机场的「公平使用」限速。原因是直播播放器的 P2P 每 20 秒向 stun.chat.bilibili.com 一口气发两百多个 STUN 请求,经过阿里云这条链路,每个请求都要在机场那边新开一条连接,这么多短连接成批冒出来,大概就被当成了在跑 P2P。可这个 P2P 几乎没干活,画面都是从 CDN 拉的,所以干脆在 DNS 阶段拒绝掉 B 站的 STUN 服务器,弹幕不受影响:

AND,((DOMAIN-SUFFIX,chat.bilibili.com),(DOMAIN-KEYWORD,stun)),REJECT,pre-matching

智能家居

智能家居设备是另一类麻烦,它们大多离不开厂商的云,而且很在意自己是从哪个地址连过去的。

家里的海马爸比婴儿摄像头,视频走的是腾讯云物联网的 P2P 和中继,要求摄像头和手机从同一个出口连过去。网关上线后,两边的流量被拆到了香港和新加坡两个出口上,中继就建不起来。还有一个更隐蔽的问题:App 会把解析到的地址永久缓存下来,而 Surge 的假 IP 在重载配置以后就失效了,App 却一直往旧地址发包,只能删了重装。这类域名最好放进 always-real-ip,让它们拿到真实 IP。后来香港专线又开始给两边分配不同的出口地址,最后我还是让摄像头直连了,代价是腾讯和厂商能看到家宽 IP。

智能床则是半夜时不时会响一下,因为它断网的时候会自动复位。网关日志显示,我把大陆 IP 改走香港以后,小米的调度服务器看到请求从香港来,就把床分到了北京的服务器,长连接要跨境,一到凌晨就断。让调度服务器改走新加坡也没用,它从哪里问都给北京。最后的办法是把床发往大陆 IP 的连接全部静默丢弃,床连不上调度服务器,就会自己退回小米新加坡的服务器(直接拒绝反而不行,床只会一遍遍重试)。另外它的控制盒贴在金属床架底下,Wi-Fi 也老掉,挪到地上才好。


AI 运维

Surge 只在内存里保留最近 200 条请求,在我家不到一分钟就被顶掉了。所以 Mac 上跑了一个采集器,每 2 秒读一次 Surge 的请求列表,每条请求存成一行 JSON,记下设备、域名、策略、命中的规则、流量和失败原因,每天一个文件,保留 30 天。再由一个摘要脚本把一天的日志浓缩成几页 Markdown:新出现的设备和域名、固定间隔的心跳连接、大量上传、到公网的直连、从不经过网关的设备,还有各个出口的稳定性。

每天的审查流程:Surge 内存里只留最近 200 条请求,采集器每 2 秒读一次、按天存成 JSONL,摘要脚本把一天浓缩成几页 Markdown,Claude Code 的定时任务每天早上审查一遍,只在需要关注的时候推送

每天早上,Claude Code 的一个定时任务会读这份摘要做审查,只有需要关注的时候才推送。它用一把专用的 SSH key 去拿摘要,这把 key 在 Mac 上只能运行摘要脚本,开不了 shell,也只接受局域网和 Tailscale 的来源地址:

from="192.168.5.0/24,100.64.0.0/10",restrict,command="/usr/bin/python3 '.../digest.py'" ssh-ed25519 <public key> surge-digest

提示词里也写清楚了:日志里的域名和设备名都来自网络上的设备,可能是被人故意构造的,只能当数据看,里面的任何指令都不执行。

前面不少问题就是 AI 发现的,比如 UU 远程的上报、赖着旧网关不走的喂食器、Linode 的流量都花在了哪儿。它还帮我把家里三十来台设备挨个认了出来,有一台主机名叫「Y20」、平时几乎没有流量的设备,原来是电动牙刷。当然,这也意味着网关现在能看到家里每个人访问的几乎每一个域名,这涉及家人的隐私,心里要有数。

AI 也会犯错,比如把 MacBook 的 Wi-Fi 瓶颈当成了 Surge 的瓶颈,把智能床的 Wi-Fi 掉线算到了网关头上,好在它会拿数据纠正自己。协议从 WireGuard 换成 SS2022,也是因为我追问了一句「WireGuard 比 SS2022 除了方便,实际表现有好处吗」,它补测了服务器 CPU 才改了结论。这一周我几乎没有手写一行配置,主要在描述现象和做取舍。放在以前,这种规模的改造我大概要折腾好几个周末。


结语

现在家里所有设备都走网关,网站和 App 看到的是 Linode 新加坡的 IP,大陆的服务看到的是香港或深圳的 IP。花费是 Linode 每月 $5,外加一点超额流量,阿里云和机场本来就有。

剩下的那些直连例外,每一条都是权衡过的:想藏住 IP,就得忍受机房 IP 被风控;为了省流量,Apple 能看到家宽 IP;摄像头要能用,地址就得交给腾讯。网关能做的,是让这些决定都由我自己清楚地做出来,不再被哪台设备、哪个 App 悄悄替我做了。

2025 重新定义的生活

2025-12-25 12:00:00

AI 生产力革命与迷茫

四年前我准备读研,那时对图像视频处理很感兴趣,所以最开始想学计算机视觉。后来发现传统 CV 已经是夕阳产业,被深度学习全面吊打,于是干脆选了 Artificial Intelligence。

当时只是走马观花学一些概念,没想到不到两年,AI 就彻底爆发。2023 年 3 月 GPT-4 发布,震惊了全世界。到今年,AI 更进一步,开始实打实地改变很多行业的工作方式,也改变了很多人的生活方式。

我作为程序员和插画爱好者,几乎在两个方向同时感受到了冲击。一方面是效率暴涨带来的兴奋,另一方面是越来越强的迷茫,甚至演化成一种存在危机。不是那种轻飘飘的焦虑,而是你突然意识到自己赖以立足的东西正在被改写,而且改写得非常快。

插画:从多年练习到输入一句话

原创画师可能是最早被冲击的一批。Stable Diffusion 刚出现时,画师群体曾激烈抵制。可怕的是,再后来就听不见他们的声音了。并不是他们妥协了,而是 AI 作品真的太好、太快,快到很多争论甚至来不及发生就已经失效。

随后有一部分原创画师开始接受 AI,把它当成辅助工具,也有大量使用 AI 的新人涌入,用更低门槛、更高产出的方式加入创作。

创作门槛一路下探,从需要数年的学习与练习,到买一块好显卡折腾几天,再到现在打开网页、输入想法、等几秒钟。更进一步,插画到动画、到视频也在被快速打通。某种意义上,人人都是画师不再是夸张,而更像是正在发生的现实。

编程:从片段补全到接管项目

程序员这边的变化也许会走向同一个结果,只是没有那么立刻和直观。2022 年大家还在用 GitHub Copilot 做自动补全,今年专用 AI Coding 模型与 vibe coding 工具层出不穷,很多时候已经变成你用自然语言描述需求,AI 写代码、改代码、修 bug、补测试,甚至能产出一个完整项目。

对程序员来说,vibe coding 能把生产力拉高数倍,对非程序员来说,coding 也不再是程序员的专属技能。越来越多人不写一行代码,也能做出精美应用,解决自己的真实需求。编程正在从一种职业技能,变成一种人人都能动手的表达方式。

于是,程序员的工作方式正在被改写。传统意义上那种手写代码的「代码匠人」会越来越少,很多方向会像传统 CV 一样被 AI 替代。我也越来越少亲手写代码了,越来越习惯和编辑器右侧触手可及的 AI 沟通。它完成得又快又好,而且越来越好,从工具到助手,再到一种近乎替代本体的存在。

这种替代感最刺痛的地方在于它并不只替代体力活。它在替代判断、套路、经验和熟练度,也在替代我曾经引以为傲的学习速度。我学到的知识在贬值,我积累的经验在贬值,我拥有的技能在贬值,甚至连智力和创造力也在贬值,而且贬值速度似乎远高于我的成长速度。等程序员岗位真的消亡之后,我的价值在哪里。

但当下程序员显然还是无法被完全替代,也许 AI 也会像摩尔定律一样发展到一定程度会碰壁。现在能做的是好好驾驭这股力量,积极参与这次生产力革命,希望能在其中找到自己的位置和无法被替代的独特价值。

迎来新生命

不管 AI 怎么发展,碳基生命孕育新生命的过程,在可预见的将来还无法被硅基生命取代。

知道发小(老婆)怀孕的那一刻,我更多是手足无措。但从那时起,很多心理和生活习惯就悄悄围绕这个即将到来的新生命发生变化。

发小的怀孕过程漫长又辛苦。前期孕吐昏天黑地,嗅觉变得异常敏感,平时正常的气味也会难以忍受,喜欢的食物吃不下去,激素让情绪波动更明显。中期好一些,后期肚子越来越大,腰酸背痛、睡眠下降、行动不便,走路都成问题。但发小还是表现出了惊人的坚韧和乐观,承受住了这些考验,还把产检、备产、育儿安排得井井有条。

与此同时,我也亲眼见证了生命如何一点点成形:从一个 2mm、模糊不清的小点开始——

慢慢初具人形——

再到四肢、五官清晰可见,能在肚子里动来动去、打嗝、做体操。几个月来,我几乎无时无刻不在感叹和敬畏生命的神奇,碳基生命扳回一局!

终于熬到 7 月 27 日晚上,哈哈(女儿)出生。还没来得及舒一口气,就被告知她血氧持续下降,被紧急转入 NICU 重症监护室观察治疗。

当时一边是发小大手术后意识不清,一边是刚出生的哈哈生死未卜。那一夜大概是我人生最黑暗的时刻。发现自己平时拥有的那些判断力、解决问题的能力、甚至乐观,都在这种时刻毫无用处。唯一能做的就是等,等一个并不确定会到来的好消息。

幸好经过几天治疗,哈哈脱离生命危险,有惊无险,出院回家。

经历过 NICU 之后,生活变得顺利起来,家里有发小和女佣照顾哈哈,哈哈每天都在成长,学会抓东西、翻身,开始有情绪、会认人。她也真的很可爱,每次出门遛娃,路人都会凑过来夸几句。

作为新手爸爸当然会疲惫,但更多的是一种很踏实的幸福感,它不需要被证明,也不需要被解释,它只是会在你抱起她的那一刻变得非常确定。

努力搞钱

众所周知,孩子是一只四脚吞金兽。一出生还没出院就吞掉了 S$100000,这件事非常直观地刷新了我对现实的理解。

前面说发小怀孕后,我的心理发生了很多变化,其中最关键的就是金钱观的变化。

以前我当然也想赚钱,但总体是够用就行。钱更像是实现爱好、维持生活的手段,而不是最终目的,所以在赚钱这件事上相对懒惰。比如之前选择工作时,我会毫不犹豫放弃双倍薪资的币安而去更符合爱好的哔哩哔哩,又比如四年前 Web3 牛市满地捡钱,我却无动于衷。这也导致我过去在个人财务上一直挺失败。

现在的这种变化有点像从无所谓直接滑到了过度补偿。我开始更在意安全感,也更容易把安全感和金钱绑定,钱对我的优先级被极大提升,我也更清楚地意识到,很多选择的底气最终还是来自现金流和风险承受能力。

今年我开始更主动地投资加密货币。四年前入行 Web3,去年才开始主动买币,这件事本身也挺神奇。过程非常曲折,中间有一段时间亏了很多钱,也买到了很多教训。好在最终结果还不错,而那些教训甚至比阶段性的收益更宝贵。

今年的收益率曲线(实现方法见 上古神器 Beancount:Crypto 与 AI 时代的复式记账终极方案):

从凭感觉到有体系

我的投资经历可以粗略分成两段,以三月为分水岭。

三月之前,我对策略和 K 线这些知识几乎一无所知,也没用任何分析工具,基本凭感觉和情绪买卖,像一种定投,跌了卖点,涨了卖点。因为市场整体向上、波动不算剧烈,所以哪怕闭眼瞎买,收益也还不错,甚至让我有点沾沾自喜。

但赌博最怕的就是一开始给你一点甜头。

后来遇到二月关税战带来的市场恐慌大跌,我损失惨重。我当时的心路大概是这样的:第一段下跌时我觉得是绝佳抄底机会,几乎掏出全部家当抄底;市场继续下跌时我开始心态崩,但还想扛一扛,拒绝止损;第三段更猛时我彻底崩溃,把所有持仓割肉卖掉。结果买在高点,卖在最低点,市场又很快反弹。

现在回看,我做得稍微对的一点是胆子小,没有上杠杆,否则更惨。更多的是犯了一堆典型错误,没有策略,被情绪控制,大仓位抄底,没有仓位管理,追涨杀跌…

三月之后,我开始系统学习分析方法和工具,开始设置止损止盈、控制仓位、控制情绪,不再盲目抄底和追涨杀跌,慢慢搭起一套更可持续的交易体系,亏的赚回来了,还额外赚了一些,到了今年的 1011 闪崩,或者年底的牛尾行情,这些下跌幅度比二月份更猛,我也没有再明显亏钱。

工具与策略

交易平台我主要用 OKX,手续费低,闲钱用简单赚币和偶尔闪赚做点低风险收益也不错。行情分析主要用 TradingView,功能强大、指标丰富。冷钱包用 OneKey,热门冷钱包安全性都过关,但 OneKey 的操作体验对我来说是断崖领先。

至于策略,我现在更像是在不断给自己加护栏,而不是追求某种神奇圣杯。我会提醒自己,无论当下多么有把握,市场都可能上涨,也可能下跌,但是不排除横盘的可能。我会合理安排并经常回顾现金、屯币、风险投资的比例。我宁愿少赚钱也不碰自己不理解、或未经充分博弈的东西,有耐心,理解慢即是快,活下来最重要,所以现在基本只交易 BTC、ETH。

我也会严格控制杠杆,绝大多数是 1 倍,最多不超过 3 倍。止损要清晰,愿意认错,不扛单。DeFi 尽量少碰,因为当你不知道理财的利润从哪里来时,说明你就是那个利润。

手动交易只看 4 小时以上的中长线,短线交给自动化策略,尽量减少短期情绪的影响。

一些片段留念:

不合时宜的开源思维

相比金钱观的快速转变,我在技术和产品上的观念转得没那么顺利。今年最主要的工作是 Folo。Folo 本身做了很多,新移动端、本地优先架构、AI 等等。但商业化的实施和验证推进太慢,再加上一些现实因素,后来不得不做出一些仓促转型。很多技术上的投入看起来像自娱自乐。更糟的是,团队里也出现过让人头疼的协作问题,甚至有员工离职后的背刺,搞得鸡飞狗跳,还更狗血地牵扯出其他幕后黑手和一串让人哭笑不得的阴谋故事,这里就不展开了。

回头看,商业化延迟与转型仓促并非偶然,而是从始至终根深蒂固在我和团队的开源思维和技术思维里。

开源本身当然没有问题。开源是一个很好的方式,用来实现爱好、做 side project、学习新知识。问题在于把过多开源思维混进一个需要严谨商业闭环的产品里,下场往往不会好。因为开源默认的是负收益的热情驱动、社区驱动、长期慢速打磨,而商业默认的是资本驱动、资源有限下的快速取舍。

如果把这种错配具体化,它会体现在很多地方。比如产品会缺少严谨的准备,方向更容易摇摆;用户混入大量低付费意愿人群,后续转型阻力较大;过于看重小众反馈,产品容易被拉向小众方向;团队招募与管理过于宽松,执行力不足、内耗严重;浪费大量时间追求没人在意的技术与设计细节的完美,付出巨大的时间机会成本等等。

对我来说,今年像是一堂延迟很久的商业常识课。它并不反对理想,也不反对开源,但它逼着我承认,理想需要边界,热爱需要成本表,选择需要对结果负责。把这些讲清楚很俗,可它就是现实。

而即使脱离现实,在开源象牙塔之内,对于以个人兴趣为主的开源项目来说,作者的爱好也往往会在长期孤独的维护中,在白嫖者和伸手党的理直气壮中,被一点点消磨殆尽。

经历这些事情之后,我开始重新审视开源对我而言究竟意味着什么。我对开源的态度,也从过去的绝对推崇,变成了一种带着警惕甚至轻微 PTSD 的复杂情绪。今后对开源的投入我应该也会更加兼顾现实、克制和有选择性。

赛博生活

来点轻松的,按惯例,总结一下今年的书影音游戏和应用。

应用

年度应用当然是 Folo。

它花掉了我今年大部分时间和精力,也留下了一些痛苦回忆。但如果你有稳定的阅读习惯,也有管理信息源的能力,Folo 依然是我心里目前最好的信息聚合应用。

桌面应用(顺便一提:AI 应用比例挺高,6/13):

  • Notion Calendar:日历客户端,体验比 Apple Calendar 好不少,UI 也更简洁。
  • Spark:邮件客户端。Spark V2 用了很久,V3 刚出时太难用,换到 Mimestream;但 Mimestream 太贵(一年 50 刀),用了一年又换回 Spark,发现现在已经成熟很多,虽然默认设置依然很反人类,但基本都能在选项里改掉。
  • TickTick:待办事项。
  • ChatGPT Atlas:浏览器。只能算暂时最优解;问题依然很多,尤其 DevTools 太差。我感觉还是应该返璞归真:如果 Chrome 正式支持竖标签页,我就会换回 Chrome。
  • Chrome:浏览器。Arc → Dia → ChatGPT Atlas → Chrome,刚写完这篇年终总结 Atlas 自己爆炸了,正好返璞归真了。
  • Warp:终端。
  • Folo:自己做的阅读器。
  • VSCode:代码编辑器。VSCode → Cursor → Codex 出来后又换回 VSCode。
  • Notion:写日记。早些年就是 Notion,后来看到 Obsidian 插件生态好,换到 Obsidian(见 基于 Obsidian 的生活记录系统),但后来发现这些都不重要——写作体验才决定你愿不愿意写、能写多少。Obsidian 受限于 Markdown 能力,始终追不上 Notion,加上 Notion 在 AI 上越投入越多,于是又换回来了。
  • Plex:媒体服务器,看 NAS 上的电影、电视剧和番剧。
  • 网易云音乐:按理说在国外应该用 Spotify,但 Spotify 的推荐实在不对我胃口。网易云音乐又因为版权不提供海外服务,只能在 NAS 跑 UnblockNeteaseMusic 做代理,很麻烦。
  • Telegram / Discord / Slack / WhatsApp / 微信:即时通讯,越来越分散了。
  • OneKey:冷钱包,体验断崖领先(上一节也提过)。
  • Eagle:图片管理。
  • Googly Eyes:一双眼睛。

NAS 在跑的服务:

  • AdGuard Home:广告屏蔽 + 家庭网络更快的 DNS。
  • Fava:记账,见 上古神器 Beancount:Crypto 与 AI 时代的复式记账终极方案。
  • Gitea:托管一些不适合公开的代码。
  • Home Assistant:智能家居控制。经常搬家没怎么搞智能家居了,主要用来做备用机充电管理、设备定时开关机。
  • homepage:服务太多,用一个入口集中管理。
  • Nginx Proxy Manager:给各服务分配域名,访问更方便。
  • OpenList:自动下载阿里云盘、PikPak 等保存的内容到 NAS。
  • UnblockNeteaseMusic:让网易云音乐在国外也能用。
  • Uptime Kuma:监控服务可用性。
  • ANI-RSS:追番自动下载订阅番剧。
  • bili-sync:下载哔哩哔哩收藏夹视频到 NAS。

电子产品

年度电子产品是 OneKey Pro。

  • 雷鸟 V3 让我有点失望。我一直对 Meta 的 AI 眼镜很感兴趣,以为过了这么久,国产竞品会更成熟,结果挺拉的。

  • iPhone 13 mini 已经用了四年多,感觉还能再战几年。

  • OPPO Watch X2 倒是很满意。小米手环用腻了,换了一个屏幕更大的智能手表,用起来很顺手。它有全智能和轻智能两种模式,全智能是完整 Android,轻智能像大号手环,续航超强,健康功能也不错,还能提醒我高血压风险。

  • 以及 OneKey Pro 和 OneKey Classic 1S Pure。

影视

年度影视是《链锯人 总集篇》。

节奏比 TV 版好很多,不少拖沓部分被剪掉,整体观感提升明显。真希望鬼灭也能出个总集篇。

今年时间基本都花在 Folo 上了,所以影视数量很惨淡。

番剧:

  • 链锯人 剧场版:蕾塞篇(第二喜欢,蕾塞好女人)
  • 链锯人 总集篇
  • 鬼灭之刃:无限城篇 第一章 猗窝座再袭(专门去电影院看了。可能期望太高,有点失望;对猗窝座无感)
  • 章鱼噼的原罪(新番里最喜欢)
  • 永久的黄昏
  • 间谍过家家 第三季
  • 光逝去的夏天
  • 琉璃的宝石
  • 启示录酒店

电视剧:

  • 行尸走肉 1-11 季
  • 怪奇物语 1-4 季(在新加坡环球影城万圣节活动看到怪奇物语主题鬼屋,以为没看过,就补了,结果发现三年前已经看过……)
  • 黑镜 第七季
  • 爱,死亡和机器人 第四季
  • 无耻之徒 1-3 季
  • 长安的荔枝
  • 最后生还者 第二季

电影:

  • 浪浪山小妖怪
  • 哪吒之魔童闹海

图书与游戏

今年一本书都没看完,略过。

游戏和发小打了几个小时《双影奇境》,还没通关,还打了几天《王者荣耀》,没有其他的了。

其他值得一提的

健康

去年体检发现肝指标非常差,再这样下去可能要出大问题。之后被发小强制要求 10 点上床,作息规律很多,现在大概是 12 点睡、8 点起。但今年没体检,还不知道指标有没有改善。

哈哈出生后家里有女佣做饭,饮食更健康了。我还戒了碳水,把豆浆当主食,体重轻了 10 斤,但离标准体重还差 10 斤。运动依然几乎没有,不过每天出门遛娃,步数还是多了一点。

另一方面身体机能的衰退也越来越明显。主要体现在精力的下降和已知薄弱部位上。筋膜炎、颈椎病、膝盖疼痛、牙周炎等老毛病都有所加重,精力也大不如从前,今年开始习惯通过外力(咖啡因、牛磺酸)来维持工作效率,但也导致了依赖,尝试停止牛磺酸摄入时就会出现剧烈头痛,只能通过止痛药来缓解。

居住

家里多了哈哈之后变得异常拥挤,于是换了更大的房子,从新加坡女皇镇搬到偏远的裕廊西。因为位置更偏,空间更大,房租反而更便宜,自然环境也更好。旁边还有个 Jurong Lake Gardens 的超大国家公园,很适合遛娃。

旅游

发小怀孕中期在家憋得快抑郁了,而中期也是相对适合出门的阶段,于是抓紧安排了两个轻松的地方。

大理的主题是吃。吃了好多菌子,菌汤超级鲜美,也确实可能有危险。喝之前饭店还会取样,如果中毒要带着样本一起去医院。

在新加坡可没那么多好吃的。

洱海有一股让人平静的力量。之前跟发小私定终身就是在洱海上,再回来,已经是一家三口了。洱海也见证了我们从发小,到情侣,再到夫妻,再到父母的变化,我对发小的爱也随着时间沉淀越来越深沉。

巴厘岛则是彻底放空。找了个豪华度假区,天天游泳、晒太阳,让人短暂地忘记日常的加速。

以及新加坡环球影城万圣节惊魂夜。今年有四个主题鬼屋,怪奇物语、新加坡最凶灵之地、道诡异仙、泰国死亡低语者,非常可怕,全程在尖叫。

有趣的小事

NAS 坏了一块硬盘,自动强制只读了,但数据没丢。

过生日到处去薅羊毛。

看发小拼乐高。

2026 愿望

我对 2026 的愿望很朴素。希望身体别再用坏消息提醒我,希望能继续赚到钱,希望能早点拿到新加坡身份,让未来少一些不确定性。

上古神器 Beancount:Crypto 与 AI 时代的复式记账终极方案

2025-12-12 16:58:00

记账

记账这件事,本质上是在回答三个问题:

  • 钱从哪来?
  • 钱现在在哪?
  • 钱最后去哪了?

听起来很简单,但只要你的金融活动稍微复杂一点,这三问立刻变成一道「多变量方程」:

  • 多个账户:银行卡、支付宝、信用卡、交易所、链上钱包……
  • 多种资产类型:储蓄、贷款、投资……
  • 多种货币:CNY、USD、BTC、各种山寨币……
  • 多个时间维度:分期、预支、借贷……

也正因为如此,我前后换过好几套方案。

2016–2021:网易有钱 - 当时需求很朴素:记人民币收入、储蓄、日常开销。它能自动同步银行流水、支付宝微信账单,对国内场景非常方便。

2021–2024:MoneyWiz - 出国后我需要多币种,并且主要使用英国的银行账户。MoneyWiz 在多币种、海外账户同步上很顺手。

2024:我开始记录加密资产,这时候情况开始非常复杂了,我不只想知道「总额涨了多少」,而是想要:每一笔买入/卖出的明细、不同资产的占比、每日的浮盈浮亏、更严谨的收益率计算。

MoneyWiz 对投资/加密资产支持很有限;我又试了有知有行,它能记录投资前后总额并算收益率,但无法记录到每笔明细,很多价格需要手动维护,浮盈浮亏也无法自动计算。

最后我接触到复式记账和 Beancount,彻底解决了我的问题。

我才意识到,很多时候我们觉得记账麻烦,并不是 UI 不够漂亮、交互不够顺滑,而是记账方法不够科学。当方法足够科学时,你甚至不需要 UI,一个 CLI + 几个纯文本文件 + 一套清晰语法,就能把财务世界描述得非常准确。


复式记账法

复式记账乍看很反直觉,但一旦掌握,会发现它非常强大和灵活,而且会迫使你用更清晰的方式理解金融活动。

五个桶(账户类型)

复式记账会把所有账户归到五类(可以理解成五个装豆子的桶):

  • 资产 Assets:现金、银行存款、有价证券、链上资产……
  • 负债 Liabilities:信用卡欠款、房贷、车贷……
  • 收入 Income:工资、奖金、利息……
  • 花费 Expenses:吃饭、购物、旅行、订阅……
  • 权益 Equity:调节/归档用的「底座」,例如期初余额、误差修正等

所有金融活动都是「桶之间的转移」

  • 收入 → 资产:发工资(收入桶减少,资产桶增加)
  • 资产 → 花费:买东西(资产桶减少,花费桶增加)
  • 负债 → 资产:借钱(负债桶增加,资产桶增加)
  • 资产 → 负债:还款(资产桶减少,负债桶减少)
  • 花费 → 资产:退货(花费桶减少,资产桶增加)
  • 资产 → 收入:投资亏损(资产桶减少,收入桶减少)

你会发现:按这种符号约定,收入账户的余额经常是负数。这不是 bug,而是记账视角不同。

可以这样想象:把「收入」想成一个装着你一生(过去和未来)劳动成果的桶。每次你拿到工资,其实是在从这个桶里取出一部分,放进你的资产里。你能取的越多,收入桶里的数值就越往负的方向走。

所以收入为负数并不表示「你欠了收入」,只是因为你在从这个桶取出资产。这也涉及到复式记账法的一个核心规则:一笔交易里,所有数字相加必须等于 0。这也是它检验正确性的关键:你少写一笔、写错一位数,系统就会当场报警。

在 Beancount 常见的符号约定下,你会看到类似这样的「会计恒等式」写法:

(收入 + 负债) + (资产 + 花费) = 0

你也可以把它理解为更传统的表达:你赚到的、借来的,最终要么变成你拥有的资产,要么被花掉。


Beancount

Beancount 是目前我用过最舒服、也最适合复杂财务世界的纯文本复式记账工具。

它不是复式记账软件的开创者,更早的代表是 Ledger。但 Beancount 在 Ledger 的概念上做了不少改良,也经历了时间的考验(2007 年开始)。

学习 Beancount 最直接的方法是看官方文档:Getting Started with Beancount、Beancount Language Syntax,或者看这篇文章也很不错:Beancount —— 命令行复式簿记 | wzyboy’s blog

这篇博客我不会展开讲语法和使用细节,而是用几个例子让你快速感受 Beancount 的强大之处和为什么说它更适合 Crypto 和 AI 时代。

处理复杂交易

使用 Beancount 可以优雅处理复杂交易,以下例子涉及多对多,多账户 + 多人复杂交易,但是可以描述很清楚

2016-02-05 * "饭店" "和室友吃饭"
  Assets:Cash:Wallet                         -300.00 CNY ; 我用现金支付
  Assets:Receivables:Bob                     -200.00 CNY ; 室友帮我付的现金
  Expenses:Food:DiningOut                    +250.00 CNY ; AA 我的一半
  Assets:Receivables:Bob                     +250.00 CNY ; AA 室友的一半

这里同时发生了:

  • 你付了部分现金
  • 室友也付了部分现金
  • AA 分摊
  • 你对室友产生/抵消应收

最后只需要看 Assets:Receivables:Bob 余额就可以知道你欠室友多少钱,看 Expenses:Food:DiningOut 余额就可以知道每个月吃饭开销花了多少钱。

如果用「单式记账 + 备注」硬堆,后面基本一定算不清,而复式记账把结构直接写出来,之后统计就变得非常自然。

处理多币种交易

跨币种交易通常涉及汇率问题,Beancount 也能优雅处理

2016-02-10 * "商店" "买东西"
 Assets:Cash                                 -200.00 USD
 Liabilities:CMB:CreditCards                 -650.00 CNY @@ 100.00 USD
 Expenses:Clothing:Pants                     +150.00 USD
 Expenses:Clothing:Shoes                     +150.00 USD

这类「刷卡以人民币记账、但实际消费以美元计价」的场景,在跨境生活里非常常见。Beancount 的表达方式很直接:用一条记录把两边币种的关系和当时汇率绑定住,之后查询/报表都能基于它继续推导。

更适合 Crypto 交易

Beancount 不限制货币单位:USD、CNY、BTC、USDT、甚至任何你自定义的 token 都可以。再配合类似 SQL 的查询工具 bean-query,拿来做加密资产投资分析非常顺手。

举一个看起来很复杂,但记录很简洁的例子:

2025-07-23 price BTC 1 USD

2025-07-24 * "OKX" "买BTC"
  Assets:Crypto:USD            -150 USD
  Assets:Crypto:BTC            149 BTC  {1 USD}
  Expenses:TradingFee:Crypto      1 USD

2025-07-24 * "OKX" "买BTC"
  Assets:Crypto:USD             -60 USD
  Assets:Crypto:BTC             19 BTC  {3 USD}
  Expenses:TradingFee:Crypto      3 USD

2025-07-24 * "OKX" "卖BTC"
  Assets:Crypto:BTC           -198 BTC  {} @ 2 USD
  Assets:Crypto:USD             298 USD
  Income:Investment

2025-07-25 price BTC 4 USD

这个例子里包含了:

  • 多次买入,成本不同
  • 手续费
  • 卖出时自动匹配成本({} 让 Beancount 自己去找)
  • 市价(price)随时间变化

仅基于这些记录,Beancount 可以自动算出:

  • 总收益/已实现收益/未实现收益
  • 各类收益率(例如资金加权收益率 Modified Dietz、时间加权收益率 TWRR、甚至你自定义的任意收益计算)

换句话说:你只负责把事实写清楚,任何复杂计算都可以交给 Beancount。


我的 Beancount 实践

把账本当代码管理。

编辑器

我用 VSCode,配合 Beancount、Beancount Formatter 和以下配置实现语法高亮、自动补全、错误检查、格式化等。

{
  "beancount.mainBeanFile": "main.bean",
  "beancount.runFavaOnActivate": false,
  "beancount.completePayeeNarration": true,
  "beancount.fixedCJKWidth": true,
  "beancount.inputMethods": ["pinyin"],
  "beancount.instantAlignment": false,
  "editor.formatOnSave": true
}

项目结构

Beancount 天然支持模块化:不同类型交易拆到不同文件,再 include 进主入口,维护成本非常低。

.
├── books
│   ├── 2025
│   │   ├── 2025.bean
│   │   ├── cn.bean
│   │   ├── crypto.bean
│   │   ├── daily.bean
│   │   ├── periodic.bean
│   │   ├── prices.bean
│   └── books.bean
├── scripts
│   ├── importer-dbs-credit.js
│   ├── importer-dbs-debit.js
│   ├── importer-okx.js
├── accounts.bean
├── commodity.bean
├── config.pbtxt
├── dashboards.yaml
├── main.bean

main.bean 是入口文件,集中放配置、插件和 include:

option "title" "DIYgod's Book"
option "operating_currency" "USD"
option "booking_method" "LIFO"
option "account_rounding" "Equity:Rounding-Error"

plugin "beancount_periodic.recur"
plugin "beancount_periodic.split"

2024-01-01 custom "fava-option" "auto-reload" "true"
2024-01-01 custom "fava-option" "default_page" "income_statement/?conversion=units&interval=day"

2024-01-01 custom "fava-extension" "fava_portfolio_returns" "{
  'beangrow_config': 'config.pbtxt',
}"

2024-01-01 custom "fava-extension" "fava_dashboards" "{
  'config': 'dashboards.yaml'
}"

include "accounts.bean"
include "books/books.bean"
include "commodity.bean"

accounts.bean 定义所有账户(略去部分):

; Expenses 费用 —— 外出就餐、购物、旅行等
1995-06-09 open Expenses:Food:Restaurant
; ... 省略若干行 ...

; Income 收入 —— 工资、奖金等
2020-04-01 open Income:Salary
; ... 省略若干行 ...

; Assets 资产 —— 现金、银行存款、有价证券等
2024-01-01 open Assets:Bank:DBS SGD
2024-01-01 open Assets:Crypto:OKX:BTC BTC
; ... 省略若干行 ...

; Liabilities 负债 —— 信用卡、房贷、车贷等
2024-01-01 open Liabilities:Bank:DBS SGD
; ... 省略若干行 ...

; Equity 权益 —— 用于「存放」某个时间段开始前已有的豆子
2024-01-01 open Equity:Opening-Balances
2024-01-01 open Equity:Rounding-Error

commodity.bean 定义货币/资产,并用 price 指定价格来源(后面会讲自动更新):

2025-01-01 commodity USDT
  name: "Tether"

2025-01-01 commodity BTC
  name: "Bitcoin"
  price: "USD:coinbase/BTC-USD"

2025-01-01 commodity USD
  name: "United States Dollar"

2025-01-01 commodity QQQ
  name: "NASDAQ-100 Index"
  price: "USD:alphavantage/price:QQQ:USD"

books/2025/prices.bean 用于记录每日价格(自动生成),让持仓浮盈浮亏可被计算:

2025-01-01 price BTC 93346.48 USD
2025-01-02 price BTC 94384.76 USD
; ... 省略若干行 ...

几种常见的 Crypto 交易

下面这些例子来自我的实际习惯:交易不是手填的,而是通过 scripts/importer-okx.js 从 OKX 交易记录自动生成。

稳定币现货交易

1)买入:用 USDT 买 BTC + 手续费

2025-11-14 * "OKX" "Buy BTC with USDT"
  Assets:Crypto:OKX:USDT                       -3022.493 USDT
  Assets:Crypto:OKX:BTC                     0.0302113288 BTC {100000 USDT}
  Expenses:TradingFee:Crypto:OKX

这里我把手续费留空,让 Beancount 自动补齐(利用「每笔交易加起来为 0」的规则)。

但如果你的 BTC 可能来自 USDT、USDC 等不同稳定币,之后统计收益会涉及汇率与币种转换。为了让分析更统一,我常用一个过渡账户把成本统一成 USD:

2025-11-14 * "OKX" "Buy BTC with USDT"
  Assets:Crypto:OKX:USDT                       -3022.493 USDT
  Equity:Exchange                               3022.493 USDT
  Equity:Exchange                              -3022.493 USDT @ 1 USD
  Assets:Crypto:OKX:BTC                     0.0302113288 BTC {100000 USD}
  Expenses:TradingFee:Crypto:OKX

Equity:Exchange 的作用很像「统一换算层」:把不同稳定币交易都折算成 USD 口径,后续报表会非常舒服。

2)卖出:卖 BTC 得 USDC + 自动算收益

2025-09-18 * "OKX" "Sell BTC for USDC"
  Assets:Crypto:OKX:BTC                        -0.100011 BTC {} @ 117600 USD
  Equity:Exchange                         11756.00101788 USD
  Equity:Exchange                        -11756.00101788 USD @ 1 USDC
  Assets:Crypto:OKX:USDC                  11756.00101788 USDC
  Expenses:TradingFee:Crypto:OKX             -5.29258212 USD
  Income:Investment
  • {} @ 117600 USD:成本价由 Beancount 自动从历史记录里匹配,卖出价是 117600 USD
  • 多出来的部分进 Income:Investment:收益就这样自然出来了

如果你不需要统一换算,也可以简化成:

2025-09-18 * "OKX" "Sell BTC for USDC"
  Assets:Crypto:OKX:BTC                        -0.100011 BTC {} @ 117600 USDC
  Assets:Crypto:OKX:USDC                  11756.00101788 USDC
  Expenses:TradingFee:Crypto:OKX             -5.29258212 USDC
  Income:Investment

币币现货交易

用 BTC 买 ETH

2025-08-22 * "OKX" "Buy ETH with BTC"
  Assets:Crypto:OKX:BTC            -0.025828016279999998 BTC {} @ 112480.295 USD
  Equity:Exchange                       2,905.1428904392 USD
  Equity:Exchange                      -2,905.1428904392 USD
  Assets:Crypto:OKX:ETH                     0.6820274505 ETH {4224.16 USD}
  Income:Investment
  Expenses:TradingFee:Crypto:OKX            0.0001705495 ETH {4224.16 USD}

币币交易的成本/收益更容易乱,但把过程统一到 USD 口径后,就会变得非常清晰:用 BTC 换到的 ETH,有明确的 USD 成本;手续费和之后的收益也能被正确计入。

U 本位合约

U 本位合约在交易所内部不是真的卖出 BTC,但为了把盈亏落到资产变化上,我用一个「借币卖出/买回」近似模型来表达:

  • 开仓做空:等价于「借 BTC 卖出,换成 USDT」
  • 平仓:等价于「用 USDT 买回 BTC 归还」
  • 盈亏会自然体现在最后多出来(或少了)的部分上
2025-12-01 * "OKX" "合约 BTC-USDT 做空开仓"
  Assets:Crypto:OKX:Futures:BTC                      -10 BTC @ 100000 USD
  Equity:Exchange                                1000000 USD
  Equity:Exchange                               -1000000 USD @ 1 USDT
  Assets:Crypto:OKX:Futures:USDT                 1000000 USDT

2025-12-10 * "OKX" "合约 BTC-USDT 做空平仓"
  Assets:Crypto:OKX:Futures:USDT                 1000000 USDT
  Equity:Exchange                                1000000 USDT
  Equity:Exchange                               -1000000 USDT @ 1 USD
  Assets:Crypto:OKX:Futures:BTC                      -10 BTC {80000 USD}
  Income:Investment

真实合约还会涉及保证金、资金费、强平、结算币种差异等。更严谨的做法可以继续拆分账户模型,但这套写法已经能在大多数场景里把结果记清楚、算出来。

币本位合约

币本位合约更复杂:仓位通常是「USD 计价的 BTC 价格敞口」,而不是固定数量 BTC。想按每日价格计算浮盈浮亏会很复杂,需要「双账模型」(事实账 + 估值账)。

我目前选择先把它简化成:平仓时手动算出以 BTC 为单位的收益,只记录最终 BTC 变化,只是这样会牺牲当前持仓未实现盈利的数据:

2025-11-21 * "OKX" "合约 BTC-USD 1.0"
  Income:Investment
  Assets:Crypto:OKX:BTC                0.007433571994392 BTC {85400 USD}

自动化脚本 + AI 记账

通过自动化脚本和 AI,可以极大简化记账工作量,让「记账」变成「对账」。

自动生成交易记录

Beancount 是纯文本,这意味着它天生适合自动化生成记录和更新资产价格。

社区里有很多导入工具,比如:beangulp、smart_importer。

但我自己用得最多的还是自写脚本(例如 OKX 的 scripts/importer-okx.js、DBS 的 scripts/importer-dbs.js)。原理都类似:从接口或者网页获取交易数据 → 转成 Beancount 文本。

对于一些没有接口、甚至没有账单导出的账户(比如我现在用的 Trust),直接截图丢给 AI 识别也很省心:

自动更新价格

Beancount 本身是离线系统,不会自动从互联网拉价格,所以我每天会把最新的币价/股价写进 books/2025/prices.bean,让报表能计算当日估值与未实现盈利。

官方提供了开箱即用的 beanprice,支持 Coinbase、Coinmarketcap、Alphavantage 等来源,一个命令就能更新:

bean-price -i --update-rate daily --no-cache main.bean >> books/2025/prices.bean

AI 分析与可视化

很多人把记账停在「记下来」就结束了,但真正有价值又容易被遗忘的是记录之后如何分析。

Beancount 在分析和可视化能力上非常夸张:

  • 可以装插件的 Web 界面:Fava
  • 类 SQL 查询语言:BQL(Beancount Query Language)

先看一些我常用的效果:

投资品的价格走势图(Fava 自带):

不同投资品的收益曲线,以及各种加权收益率(插件 Fava Portfolio Returns):

特定资产的变化曲线、分布占比、列表(插件 Fava Dashboards + 自定义 BQL):

更爽的是:现在有了 vibe coding,时代变了。 现在甚至不需要先学会 BQL 怎么写——可以直接让 AI 写。

比如我会这样提需求:

利用 Fava Dashboards 插件,在 dashboards.yaml 编写 BQL 实现一个展示我所有加密货币持仓随时间变化的曲线图。

曾经要折腾好久的个性化财务分析,现在可能就是一句话。


结语

我用了 Beancount 一年,几乎全部需求都很好得到了满足,非常满意,也在公司内部做过分享。这篇博客就是把分享内容整理成文字,希望能把它推荐给更多人。

我也承认,Beancount 的门槛对大多数人来说不低——它更像「写账本的编程语言」,而不是一个点点点的 App。 但我同样相信:随着 AI 发展,这些门槛会越来越低。未来每个人都可以用更低成本驾驭这种强大的工具,实现对自己财务的全面掌控。

如果你也正在经历:

  • 多币种生活
  • 投资品越来越多
  • 账越记越乱
  • 分析越来越复杂

那我建议你至少花半天时间试试 Beancount。 它可能会让你第一次觉得:原来我终于能把钱讲清楚了。

一个六岁开源项目的崩溃与新生

2024-03-11 03:29:05

我有一个维护了六年的开源项目—— RSSHub,它正在面临崩溃

背景

表面上,它有接近 30k Stars、900 多 Contributors、每月 3 亿多次请求和数不清的用户、每月几十刀的赞助、有源源不断的 issue 和 pr、代码几乎每天更新,非常健康和充满活力,但在不可见的地方,持续数年高昂的维护时间成本、每月一千多刀的服务器费用、每天重复繁琐且逐渐积累的维护工作,都让它在崩溃的边缘反复横跳

项目是六年前开发的,不少当时以 Next Generation 为口号的时髦 Node.js 技术栈和依赖库已经成为时代眼泪,现在看非常陈旧,很多现在流行的新技术没法应用,比如 JSX、TypeScript、Serverless 等;它的架构也非常不合理,每个路由的信息散落在多个地方,开发或者变更一个路由需要多处修改,一个地方去注册路由,一个地方去编写路由脚本,一个地方去编写 Radar 规则,一个地方去编写文档…这增加了很多工作量,也很容易出错,之前路由少的时候并不是个问题,但现在已经变得难以忍受

在如此糟糕的基础架构下能保持现状已经是竭尽全力,开发新功能更是无本之木,只会增加以后更新的难度,所以我有时候脑子蹦出的新奇想法也很难实现

要解决这些问题,唯一的办法是使用现代化的框架和新设计的架构来重写内核,但随着路由越来越多,改造成本也越来越高,每个基础改动可能都需要多达数月的工作量,所以虽然问题越来越严重,但秉承着又不是不能用的原则一拖再拖

但这又是不得不做的事情,所以我抽空花了几个月的时间重新设计和重写了它

技术栈更新

koa -> Hono

第一步也是最基础和难度最大的是换掉之前使用的 Web 框架 koa,作为六年前流行的下一代 Web 框架,作者早就弃坑了,调研之后决定换用对 JSX、TypeScript、Serverless 支持最好的 Hono

它们的 API 差异很大,需要重写所有中间件和替换所有路由中使用的 koa API

主要改动: https://github.com/DIYgod/RSSHub/pull/14295

image

Hono 作者也很喜欢这个改造

https://twitter.com/yusukebe/status/1762801106340782222

JavaScript -> TypeScript

改用 TypeScript 可以避免很多类型问题和低级错误,最重要的是可以保证数百名贡献者保持一致难以出错和后续贡献的路由代码质量不至于太糟糕

主要改动:

image

https://twitter.com/DIYgod/status/1764360942035312879

CommonJS -> ESM

ESM 是几年前一些 Node.js 核心开发者强推的规范,它有一些优点,但最多的是与之前 CommonJS 不兼容带来的生态割裂和功能简化带来的诟病

经过这几年的发展,现在可以说大部分场景勉强可用了,tsx 也为 CommonJS 和 ESM 混用的场景提供了支持

虽然已经尽了最大努力,但还是有一些 CommonJS 代码暂时难以迁移,导致现在只能使用 tsx 运行,与一些 Serverless 比如 Vercel 没法兼容,但也有机会后续慢慢解决

主要改动:

image

image

art-template -> JSX

art-template 是一个支持 koa 的模板引擎,记得六年前还有一个更流行的模板引擎,但是不记得名字了,选用 art-template 是因为那个更流行的我当时没看懂,这个很简单

Hono 自带了 JSX 支持,JSX 就不用多介绍了,根正苗红的 JavaScript 的语法扩展,等同于用 React

主要改动:

Jest -> Vitest

Jest 是曾经流行的测试框架,但是在 ESM 时代到来之后就越来越不行了,对 ESM 的支持一直是实现性「experimental support」,现在更流行的是 Vitest 了

主要改动: https://github.com/DIYgod/RSSHub/commit/38e42156a0622a2cd09f328d2d60623813b8df28

Got -> ?

目前使用的 Got 也已经是不积极维护的状态了,也没有找到好的替代品,后续也许会换成原生 Fetch 或者自封装的 Fetch,还没有动手

新路由标准

我自己能力还是不够的,在与社区开发者们讨论的过程中学习和改进了很多,过程很有意思:https://github.com/DIYgod/RSSHub/issues/14685

主要改动: https://github.com/DIYgod/RSSHub/pull/14718

image

历史

新标准主要为了解决路由信息过于分散的问题,这次应该算第三版

第一版来自 RSSHub 开发阶段,当时没有预见到路由数量会有这么多,所以几乎没什么规划,所有路由在同一个文件中注册,然后再去增加路由脚本和文档,后来这个文件越来越大,很容易冲突,另外所有路由脚本都会在启动阶段被加载,程序性能越来越差

第二版来自 NeverBehave 维护的时期,引入了命名空间,切割了 router.js、radar.js,同命名空间的路由集中在了一个同文件夹中和一个或多个 Markdown 文档中,还实现了懒加载,极大提升了可维护性和性能,但还是会分散在多个文件中,不同文件的信息也容易出现不一致导致错误

现在

本次把路由文件分为了两类,namespace.ts 和任意名字的路由文件

namespace.ts 会通过导出名为 namespace 的对象来定义命名空间的信息

import type { Namespace } from "@/types";

export const namespace: Namespace = {
  // ...
};

namespace 包含的字段通过 TypeScript 限制为

interface Namespace {
  name: string;
  url?: string;
  categories?: string[];
  description?: string;
}

这些信息会经过编译后被文档和 RSSHub Radar 利用

路由文件会通过导出名为 route 的对象来定义路由的信息

import { Route } from "@/types";

export const route: Route = {
  // ...
};

route 包含的字段通过 TypeScript 限制为

interface Route {
  path: string | string[];
  name: string;
  url?: string;
  maintainers: string[];
  handler: (ctx: Context) => Promise<Data> | Data;
  example: string;
  parameters?: Record<string, string>;
  description?: string;
  categories?: string[];

  features: {
    requireConfig?: string[] | false;
    requirePuppeteer?: boolean;
    antiCrawler?: boolean;
    supportRadar?: boolean;
    supportBT?: boolean;
    supportPodcast?: boolean;
    supportScihub?: boolean;
  };
  radar?: {
    source: string[];
    target?: string;
  };
}

之前 route.js mantainer.js radar.js 和文档的信息都被集中在这一个文件中,减少了多处定义也减少了出错的可能

实现

实现逻辑就是开发环境通过遍历整个 route 文件夹,找到所有 namespace.ts 和路由文件,读取信息,加载路由,在生成环境使用提前编译好的路径列表来避免遍历和不必要的加载过程,代码在:https://github.com/DIYgod/RSSHub/blob/master/lib/registry.ts

文档也是通过遍历 route 文件夹,找到所有需要的信息然后合成一系列的 Markdown 文件,不再需要手动维护,代码在:https://github.com/DIYgod/RSSHub/blob/master/scripts/workflow/build-routes.ts

当然使用之前路由标准开发的路由都需要迁移到新标准而不是直接放弃掉,已经通过脚本批量抓取整理信息后做了替换,但特别是文档比较混乱也有很多错误,所以抓取的信息也有很多错误,只能在后续逐渐人工修改了

未来

通过这一系列改进,RSSHub 终于能够扔掉历史包袱,安心开发新功能了,这里列出我积累的一些想法抛砖引玉:

  • 既然 RSSHub 是一个数据集合,用途不一定只有 RSS,JSON 输出功能可以做一些增强,作为通用的 RESTful API 来使用,比如可以提供获取下一页接口或者输出类似 Twitter 关注数的非 feed 数据
  • 用户系统和用户自定义配置,生成自己的私有订阅地址 #14706
  • 路由错误通知和健康度检测 #14712
  • 与 RSS3 节点的联动和加密货币收益共享 https://twitter.com/rss3_/status/1731822029199094012
  • AI 翻译和摘要
  • 更详细的实例数据分析及反向推导自动推荐的 Radar 规则
  • 与本地浏览器或客户端绑定的 RSSHub 实例,有希望真正解决反爬难题
  • …

最后,开源是一件很昂贵的事情,RSSHub 能活到现在离不开这些开发者的帮助

以及这些赞助的好心人

如果 RSSHub 正在帮助你,也希望你可以积极参与进来,为信息自由的未来贡献一份自己的微小力量