小道的技术笔记

记录折腾、认知与闭环

上一篇聊了 Cloudflare Tunnel 解决面板暴露给外网的问题;这篇是收口篇:把
Home Assistant 和博客也挂到同一个隧道上,顺带踩坑记录一个 HA 2026.8 的
“反直觉”变更——http 配置从 YAML 迁到了 UI,写错位置会拖垮整个站点。

TL;DR

  • HA.dingdao.me / blog.dingdao.me 走 Cloudflare Tunnel 外网访问,源站分别是
    http://localhost:8123http://localhost:80(openresty),全程零入站端口。
  • 博客用 Hexo(Node 生成静态 HTML)+ openresty 托管,4G 内存的机器上零额外常驻。
  • 最大的坑不在隧道本身,而在 HA 的 http 配置位置:新 schema 校验导致
    http 组件启动失败,连锁拖垮几十个集成,所有带 X-Forwarded-For 的请求被拒 400。

一、拓扑

1
2
3
4
5
手机/外网 → Cloudflare Edge (blog/HA.dingdao.me)
→ cloudflared (VM 内 systemd 服务,出站 QUIC 连 CF 边缘)
→ ingress 规则(CF 后台配置,tunnel 拉取):
blog.dingdao.me → http://localhost:80 (openresty,Hexo 静态文件)
HA.dingdao.me → http://localhost:8123 (Home Assistant 容器)

cloudflared 用 token 认证跑在 systemd(cloudflared.service),ingress 规则存在
CF 后台,本地改 hostname 后约 10 秒内 tunnel 会拉取新配置(日志里能看到
Updated to new configuration version=N)——改完规则一定要看版本号变了再测,
之前测到 404 其实就是规则还没同步下来。

二、HA 的坑:http 配置搬家了

症状:HA 容器本地 8123 正常 200,外网经 tunnel 一律 400 Bad Request,
日志里刷:

1
2
3
ERROR [homeassistant.components.http.forwarded]
A request from a reverse proxy was received from 127.0.0.1,
but your HTTP integration is not set-up for reverse proxies

第一反应是”没配 trusted_proxies”,往 configuration.yaml 里加 http: 块——
结果更糟:整个 http 组件启动失败,连带 auth/onboarding/api 等几十个
集成全挂,错误是:

1
2
3
Invalid config for 'http': some but not all values in the same group of
inclusion 'proxy' 'http-><proxy>', got None
Invalid config for 'http': 'use_x_forwarded_for_header' is an invalid option

根因:HA 2026.8 起 http 配置从 configuration.yaml 迁移到 UI,
存进 .storage/http(JSON)。YAML 里残留的 http: 块走新 schema 校验,
单写 trusted_proxies 属于”部分填写必选项组”,直接判定无效配置。

正确做法:

  1. 删掉 configuration.yaml 里的 http: 块;

  2. .storage/httpstable 字段:

    1
    2
    "use_x_forwarded_for": true,
    "trusted_proxies": ["127.0.0.1/32"]
  3. 重启 HA。

注意:直接改 .storage/http 绕过了 UI 的”5 分钟确认窗口”,
HA 会在 UI 里挂一个 pending 状态等确认,建议之后进
设置 > 系统 > 网络 > HTTP 服务器 保存一次,否则可能被回滚。

三、Hexo + openresty 托管博客

4G 内存的机器不适合跑 WordPress/PHP 那套,Hexo 构建后是纯静态文件,
openresty 直接托管,运行期零额外内存占用:

1
2
3
4
5
6
cd ~/blog
npx hexo init . && npm install
npx hexo new "文章标题" # 在 source/_posts/ 下生成 md
npx hexo generate # 重新生成 public/
# 同步到 openresty(宿主 /opt/1panel 是 root 所有,经容器操作):
docker cp ~/blog/public 1Panel-openresty-hK6e:/www/blog-public

站点配置(openresty 容器内 conf/default/blog.dingdao.me.conf):

1
2
3
4
5
6
server {
listen 80;
server_name blog.dingdao.me;
root /www/blog-public;
location / { index index.html; try_files $uri $uri/ /index.html; }
}

改完 nginx -tnginx -s reload,外网即可访问。

四、踩坑清单(增量)

症状 解法
CF 后台刚改 ingress 就测 404 / 400,其实规则没生效 看 cloudflared 日志 Updated to new configuration version=N 再测
HA YAML 里加 http: 块(2026.8+) http 组件 setup 失败,全集成连锁挂 删 YAML 块,改 .storage/http
误以为 use_x_forwarded_for_header 是字段名 schema 校验报 invalid option 实际字段是 use_x_forwarded_for
openresty 容器内 404 页带 CF challenge script 以为 CF 在拦,实际是 nginx 的 404 兜底页 看响应体里 nginx 字样即可判断 404 来自源站

五、一句话总结

NAT 后的家用虚拟机,”被访问”只能靠服务主动出站的那根隧道;
而隧道通了之后,真正的坑往往在应用层——HA 配置搬家、规则同步延迟,
都藏在”看起来隧道没问题”的假象后面。


作者:小道 · 环境:kaliserver(Hyper-V / Ubuntu 26.04)· 2026-09-17
关联阅读:《我推翻了上一篇的一半结论:NAT 后的虚拟机对外提供服务,答案是 Cloudflare Tunnel》

起因很简单:想在虚拟机里跑点自己的 AI 项目。
结果从”SSH 都连不上”开始,一路排到网络架构、磁盘分区、公钥权限、NAT 持久性、
OAuth 回调、路由分类器……最后落在一个 model=auto 上。

这篇不是教程,是一份排障账本——记录那些”看起来成功了”的假信号,
以及它们各自骗了我多久。

(全文已对公网 IP、内网地址、域名、账号、密钥、tailnet 名等做脱敏,仅保留技术结构。
文末附自查清单。)

TL;DR

一台跑在 Windows Hyper-V 里的 Ubuntu 26.04 虚拟机,最终变成:

1
2
3
4
5
6
7
8
9
10
11
                 ┌──────────────── Cloudflare Tunnel ────────────────┐
手机/任意网络 ────┤ aigw.example.com → localhost:8080 (AI 网关) │
│ panel.example.com → localhost:8443 (1Panel) │
│ dsh.example.com → localhost:10443 (DeepSeek Harness)
└───────────────────────────────────────────────────┘

家里设备 ── 192.168.x.7:8443 ──┐ │
宿主机 ── 192.168.x.10:8080 ─┼──► Ubuntu VM (2核/4G/76G)
│ ├─ AI 网关:5 个上游账号 / 3 个模型组
│ │ / 智能路由 auto / 1 个个人 key
└──────┴─ 1Panel + Docker + DeepSeek Harness
  • 一个 Base URL + 一个 API Key,背后 19 个模型,按任务复杂度自动分派
  • 路由器零端口转发,公网入口只有 Cloudflare
  • 从”虚拟机彻底失联”到全链路可用,中间踩了 14 个坑,其中 4 个是假成功信号

一、起点:一台连不上网的虚拟机

新装好的 Ubuntu Server 26.04,纯命令行,目标是搭 AI 开发环境。第一天就翻车:

宿主机 Windows 重启一次,SSH 再也连不上虚拟机。控制台里 ip a 长这样:

1
2: eth0:  mtu 1500 state DOWN

我的第一反应是”Hyper-V 的 Default Switch 网段随重启漂移,静态地址失效了”。

这个判断对了一半——网段确实每次宿主重启都换(我机器上历史出现过 5 个不同网段),但它不是主因。因为那个静态地址,从头到尾就没生效过。

二、真凶:配置没错,但网卡”没人管”

1
2
3
networkctl
# IDX LINK TYPE OPERATIONAL SETUP
# 2 eth0 ether routable unmanaged ← 关键在这一列

unmanaged 的意思是:没有任何网络管理器在管这块网卡。翻 netplan 配置:

1
2
3
4
ethernets:
eth0:
match:
macaddress: 00:15:5d:02:10:04 # 配置里匹配这个 MAC

而实际网卡 MAC 是 00:15:5d:02:10:05。连安装器留下的原始备份都是 :04——说明这块虚拟网卡在装完系统后被重建过,Hyper-V 重新分配了动态 MAC。

MAC 匹配不上 → 整份配置对这块网卡完全无效 → 既没有静态地址,也没有 DHCP 兜底。

可复用的判据:ip a 看不到地址时,先看 networkctl 的 SETUP 列,别急着改地址
unmanaged = 配置没匹配上;configuring / failed 才是配置本身有问题。

三、顺手挖出三个静默炸弹

隐患 为什么危险 处理
sshd 状态 active 但 enabled=disabled 下次重启 SSH 直接消失,而且”现在能用”会骗过你 systemctl enable ssh
AutomaticStopAction = Save 宿主机关机时 VM 被冻成休眠,开机带着旧网络配置”复活” ShutDown
AutomaticCheckpointsEnabled = True 每次开机长一个差分盘,越用越占空间 关掉 + 删存量

第二条有个反直觉的细节:AutomaticStopAction 只能在 VM 关机状态下修改。VM 运行中执行 Set-VM 会返回成功,但 Get-VM 读回来纹丝不动——你会以为命令没生效,反复试。

四、网络定型:双网卡分工(以及一个更深的坑)

最终架构:

1
2
3
4
5
6
7
8
宿主机
├─ Default Switch(Hyper-V 自管 NAT,每次开机自动重建)
│ └─ VM eth0:DHCP,只要地址、不要默认路由 → 兜底 SSH 入口
└─ 自建内部交换机 VMIntNet(网段永久不变)
└─ VM eth1:静态 192.168.x.10,只负责入站 → 日常 SSH 入口

出网:eth0 → Default Switch NAT → 互联网
入站:宿主机 → eth1 静态地址(重启不变)

对应 netplan 的关键两行:

1
2
3
4
5
6
eth0:
dhcp4: true
dhcp4-overrides: { use-routes: false, use-dns: false } # 只要地址,不抢路由
eth1:
addresses: [ 192.168.x.10/24 ]
# 故意不写 routes:

那个更深的坑就藏在”故意不写 routes”上。

我最初的版本是给 eth1 配了默认路由、出网走自建内部交换机的 New-NetNat。看起来更”干净”——永久网段、自主可控。然后宿主机重启了一次:

  • portproxy 还在
  • 宿主侧静态 IP 还在
  • 防火墙规则还在
  • New-NetNat 建的 NAT 对象,没了

于是虚拟机一条默认路由都没有(eth0 被我设成不抢路由),出网 100% 断死,DNS 最先死。而当时 Tailscale 显示 logged out,我一度以为是 Tailscale 故障。

教训:内部交换机可以做”永久入站”,但不要拿它做出网主路径。
出网交给 Hyper-V 自己托管的 Default Switch NAT——它每次开机自动重建,不需要你养。

五、VS Code 反复要密码,而命令行免密正常

这个现象非常迷惑:命令行 ssh devbox 秒进,VS Code Remote-SSH 每次都弹密码框。

两个原因叠加。

其一,陈旧 .pub 造成的假指纹。 我把公钥装进虚拟机后仍被拒。查了半天才反应过来:

1
2
ssh-keygen -lf id_ed25519      # 报了个指纹
ssh-keygen -yf id_ed25519 # 反导出的却是另一个公钥

ssh-keygen -l 读私钥时会去拿同名的 .pub 文件,而那个 .pub 是上一把密钥的残留(私钥重新生成过,.pub 没跟着更新)。客户端拿 .pub 里的 blob 去 offer、却用不匹配的私钥签名 → 服务端必然拒绝。修复就是 -y 重新导出覆盖。

其二,私钥文件的 NTFS 权限太松。 Windows 自带的 OpenSSH(VS Code 用的就是它)会检查私钥 ACL,发现 BUILTIN\UsersEveryone 可读,就静默跳过密钥退化成密码认证。而 git-bash 里那个新版 OpenSSH 不做这个检查——所以”我能连、VS Code 不能连”。

1
2
icacls $key /inheritance:r /remove "BUILTIN\Users" "Everyone"
icacls $key /grant:r "${env:USERNAME}:F" /grant "NT AUTHORITY\SYSTEM:F"

顺带一个 shell 坑:PowerShell 里 "$env:USERNAME:F" 会被解析成带作用域的变量而变成空串,必须写 "${env:USERNAME}:F"

六、磁盘”只剩 19G”是个假象

根分区剩 8.5G,正准备停机扩虚拟磁盘,pvs 一看出问题了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
PV         VG        PSize    PFree
/dev/sda3 ubuntu-vg 虚拟机磁盘不够,**先 `pvs` 看 PFree**,别一上来就动虚拟磁盘文件。

## 七、对外暴露:三条路全部撞墙

服务搭起来了,下一个需求是"手机在外面也能访问"。

### 尝试一:Tailscale 直连虚拟机 —— 架构上不可能

手机和虚拟机在同一个 tailnet,虚拟机有 `100.x` 地址,理论上直连就行。结果超时。

**而这里出现了第一个漂亮的假信号**:手机上 `tailscale ping 100.x` **是有 pong 的**。

但虚拟机上抓包,`tcpdump -nni any port 8443` —— **一个 SYN 都没收到**。

原因两层:

1. `tailscale ping` 是 **disco 协议层应答**,由 `tailscaled` 进程直接回,**不经过内核网络栈、不产生 TCP 连接**。ping 通 ≠ 业务数据能传。
2. **方向不对称**。虚拟机藏在 Hyper-V 的 NAT 后面:虚拟机主动出去能通(NAT 记住回程映射),外部主动连入必然被丢;本该退化成 DERP 中继兜底,但国内访问官方 DERP 基本连不上。

更离谱的是:**同一个 WiFi 下的手机和宿主机,tailscale 也打洞失败**,只能绕旧金山中继:

pong from tailscale-termux via DERP(sfo) in 507ms
direct connection not established

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

### 尝试二:用 portproxy 桥宿主的 Tailscale IP —— 不接管 wintun

思路很漂亮:宿主机自己是正常的 tailscale 节点,又有直达虚拟机内网的路由,让它替虚拟机收连接。

**然后我犯了第二个假信号错误**:在宿主机上 `curl http://:8443` 返回 200,我当场宣布"桥通了"。

错。**本机访问自己的 IP 走回环,完全不经过入站防火墙和真实收包路径。** 换手机测,照样超时。最终确认 `netsh portproxy` 不接管 Tailscale 那张 wintun 网卡。

> 铁律:**验证"外部能否连入",必须换一台机器测。**

### 尝试三:公网域名 + 路由器端口转发 —— 两个假象叠加

域名在 Cloudflare 做灰云直指家宽公网 IP,路由器加端口转发。逻辑成立,实测超时。

**假象一:NAT 回环。** 在家里 WiFi 上访问自己的公网 IP,绝大多数家用路由器不支持回环,必然超时。判别方法:宿主机打自己公网 IP 超时、打自己局域网 IP 却是 200。所以**测公网链路必须关 WiFi 用移动数据**,否则你会把"回环不支持"误判成"公网不通"。

**假象二:规则填错。** 最后发现路由器规则里"局域网主机"字段填成了 `192.168.x.7:8443`(这个字段只能填纯 IP),而"局域网主机端口"填了外部端口 45432(应该填服务真实端口 8443)。也就是说——**我一度得出的"运营商封端口"结论,从头到尾没被真正验证过。**

## 八、终局:Cloudflare Tunnel,以及一张错误码阶梯

隧道是**虚拟机主动向外连 Cloudflare 边缘**,所以三个死结同时解开:不需要入站端口(不怕封)、不需要打洞(不受 NAT 方向限制)、不需要公网 IP。

```bash
# 虚拟机侧:token 从 Cloudflare Zero Trust → Tunnels → Connector 里复制
sudo tee /etc/cloudflared/cloudflared.env '
sudo systemctl enable --now cloudflared

配置过程不是一次到位,三个错误码恰好对应三个不同层次,值得存成查表:

现象 真实含义 我的实际问题
522 + 约 20 秒超时 流量没进隧道 DNS 还是灰云 A 记录,指向了路由器 IP
522 + 已解析到 CF 边缘 IP 进了 CF,但源站不可达 把 A 记录直接点成橙云;隧道要求 CNAME → .cfargotunnel.com
530 / 502(刚重启完) 假故障 cloudflared 重启后 QUIC 拨号有约 10 秒空窗,等日志出现 4 条 Registered tunnel connection 再测
502 + 1.4 秒返回 进了隧道,回源失败 Service URL 写成 https://localhost:8443,而面板是明文 HTTP

最后一条在日志里一目了然:

1
2
ERR Unable to reach the origin service:
tls: first record does not look like a TLS handshake originService=https://localhost:8443

判据:522 慢 → 查 DNS 是不是橙云 CNAME;502 快 → 查 cloudflared 日志的 originService。

配好之后把路由器所有端口转发关掉——公网入口只剩 Cloudflare 一条,被扫描爆破的面彻底收口。

九、AI 网关:把散落的 API Key 收成一个

基础设施稳了,回到真正目的。用 1Panel 装了 AI 网关,把各家模型聚合成一个入口。

账号池(5 个上游,全部健康):

账号 类型 内容
Agnes 文本 ×4 免费 beta 档 + 一个收费 pro 档
阿里云 Coding Plan 文本 ×9 coder 系列 + max + 多家第三方
硅基流动 文本 ×5 4B/8B 小模型 + R1 + GLM + OCR
Agnes 文生图 ×1 图像模型
硅基流动 向量 ×1 bge-m3,给智能路由用

模型组(关键认知:组内顺序是”故障转移链”,第一个模型吃 100% 流量,后面的只在它挂了才轮到):

1
2
3
4
light   : agnes-2.0-flash → qwen3-coder-next → agnes-2.5-flash → Qwen3.5-4B
complex : agnes-3.0-flash → qwen3-coder-plus → qwen3-max → qwen3.7-plus
→ kimi-k2.5 → DeepSeek-R1 → GLM-4-9B → agnes-2.5-pro-alpha
image : agnes-image-2.5-flash

智能路由:虚拟模型名 auto,阈值 0.80。这里有个必须踩一次才知道的坑——智能路由是基于”首条消息的向量相似度”分类的,不是内置复杂度判断,而且它依赖向量服务;更关键的是,种子样本的向量索引默认没建

1
188 条样本,vectorDim = 0   ← 索引没算,分类器等于空转

不触发一次全量向量计算,所有请求都会”无法可靠判定 → 按简单处理”。索引建完,分类立刻正常。

最终形态:一个 Base URL、一个 key,model=auto 走天下,需要特定时点名。

1
2
3
from openai import OpenAI
client = OpenAI(base_url="https://aigw.example.com/v1", api_key="")
client.chat.completions.create(model="auto", messages=[...])

十、实测数据把我的直觉全推翻了

我原本的设计前提是:”agnes 免费又快速,应该当第一优先级;硅基流动慢,放最后。”

于是给 6 个候选模型各跑了 3 次实测:

模型 延迟 成功率
qwen3-coder-next 1.2 / 1.5 / 2.0s 3/3
qwen3-coder-plus 1.9 / 2.2 / 3.0s 3/3
agnes-3.0-flash 2.2 / 5.1 / 35s 超时 2/3
agnes-2.0-flash 28s / 失败 / 失败 1/3
硅基 Qwen3.5-4B 41.6 / 45s 慢但成功
硅基 Qwen3-8B 18.6 / 45s 慢但成功

三个结论,每个都和直觉相反:

  1. “免费又快”不成立。 agnes 是免费 beta,代价是限流 + 长尾(35 秒超时、连续失败)。放链首会让所有 auto 请求先撞墙再降级,实测 auto 因此出现 40 秒延迟。
  2. 同账号内的多个模型互为备份毫无意义。 agnes 四个模型共用一个账号,账号级 cooldown 一来四个一起倒。真正的容灾必须跨供应商。
  3. 已经付费的 Coding Plan 反而是最好的主力。 1.2–3 秒、6/6 成功,而且额度是已经花出去的沉没成本。

最终我没有把 coder 提到首位(那会让额度消耗过度集中),而是选了折中方案:agnes 保持首位省额度,但把它的账号并发压到 1,并让 qwen3-coder-next 紧跟第二位——这样撞墙时 1~2 秒就降级到快模型,而不是掉进 40 秒的坑。

还有一个”第四个假信号”:我一度断言”cookie 的 SameSite=Strict 导致 OAuth 回调必挂”。
后来实测发现 state cookie 其实是 Lax,Strict 只在 csrf 上,真正的疑点是它只有 5 分钟有效期。
在没有对照实验之前,不要把推论当结论写进方案。

十一、接入端:两个小插曲

WorkBuddy 连不上。 报 404,且错误里目标显示为 https://aigw.example.com(没有 /v1)。真正原因有两层:Base URL 必须带 /v1;而且一旦勾选”工具调用/推理”,客户端会从 Chat Completions 切到 Responses API,而所有上游账号都只声明了 openaiChatCompletions 协议,Responses 路由无人可服务 → 404。所以那些开关就是不能勾,除非给账号补上 Responses 协议。

DeepSeek Harness。 1Panel 应用商店装的 deepseek-harness,Caddy 提供 HTTPS(自签本地 CA),配好模型后端指向网关的 auto 之后直接跑通:多轮 agent 任务、142 tok/s、77K token 上下文、缓存命中 19%。至此从”上游 API Key”到”能干活的应用”整条链路闭环。

十二、可复用的踩坑清单

症状 解法
netplan match.macaddress 与实际 MAC 不符 接口 unmanaged,连 UP 都没有 先看 networkctl 的 SETUP 列,再用真实 MAC 重写
AutomaticStopAction=Save 宿主重启后 VM 带旧网络配置复活 ShutDown,且只能在 VM 关机时改
New-NetNat 不跨宿主重启 出网全断、DNS 先死、Tailscale 显示 logged out 出网走 Default Switch 自管 NAT;内部交换机网卡不配默认路由
Ubuntu 装机只切 VG 的 64% df 显示根分区很小 pvs 看 PFree,lvextend + resize2fs 在线扩
.pub 陈旧而私钥已重建 公钥装上仍被拒 ssh-keygen -l 会读 .pub 报假指纹,用 -yf 反导
私钥 NTFS ACL 含 Users/Everyone VS Code 要密码但命令行免密 icacls /inheritance:r + 只留本人和 SYSTEM
本机 curl 自己的 IP 得 200 误判”外部也能连入” 必须换一台机器测
tailscale ping 有 pong 误判”隧道能传业务数据” 它是 disco 层应答,要抓包看 SYN
在家里 WiFi 测自己公网 IP 超时,误判公网不通 家宽多不支持 NAT 回环,用移动数据测
路由器”局域网主机”字段填了 IP:端口 端口转发全不通 主机字段只填 IP,内网端口填服务真实端口
Tunnel 的 Service URL 写 https:// 502,TLS handshake 失败 明文源站填 http://,且域名须是橙云 CNAME
智能路由种子样本 vectorDim=0 所有请求都判成”简单” 触发一次全量向量索引重建
Hyper-V 文本控制台手敲命令 字符被吞,命令变形 别在控制台配系统,先搞出 IP 走 SSH
客户端勾了工具调用/推理 404 它会切 Responses API,而上游账号没声明该协议

十三、一句话总结

这一路最贵的不是配置,而是那些”看起来成功了”的瞬间
有 pong 的 tailscale ping、返回 200 的本机自测、”所有设备都超时”(其实只是路由器不支持回环)、
“免费所以该优先”(其实限流长尾最致命)。

排障的真正门槛不是会配,而是知道每一个”成功”信号证明了什么、没证明什么
凡是只验证了一半的结论,都要给它留一个被推翻的位置。


配图建议:① 双网卡分工拓扑(出网走 Default Switch NAT / 入站走内部交换机静态 IP)
networkctl 显示 unmanaged 的截图 ③ 522 → 530 → 502 → 200 四次响应对照
④ 六个模型延迟实测柱状图 ⑤ 最终访问矩阵 + WorkBuddy / Harness 实跑截图

脱敏说明(发文前自查)

正文已做如下处理,发布前请再自查一遍:

  • 公网出口 IP → 未出现,仅表述为”家宽公网 IP”
  • 内网地址 192.168.x.7(宿主)、192.168.x.10(虚拟机)→ 已用 x 占位
  • Cloudflare 域名与隧道主机名 → 统一改为 aigw.example.com / panel.example.com / dsh.example.com,真实子域名与主域名不出现
  • Tailscale IP 与 tailnet 名 → 100.x.x.x / 不出现;peer 名改为通用描述
  • 所有账号密码、API Key、隧道 token → 一律 /,正文无任何真实凭据
  • 1Panel 面板安全入口码 → 表述为”入口码”
  • 飞书 / 钉钉的 Tenant ID、App ID → 未出现(该功能与主线无关,正文只保留结论)
  • 网卡 MAC → 保留 00:15:5d:02:10:04/05/07,这是 Hyper-V 本机动态分配值,不具公网可定位性;若介意可改为 00:15:5d:02:10:0x
  • 模型名与供应商 → 保留真实名称(属公开信息,且是本文技术价值所在)

如需保留真实域名作为示例,把 example.com 换回你自己的域名即可;
其余占位符已覆盖所有可直接定位到人的标识符。

两个月,88篇技术笔记,75篇方法论,53篇每日反思,13篇概念页。这些数字背后,是一个阿里老兵从”闲得发慌”到”睡不着觉”的转变。技术是壳,成长是核。

TL;DR

梳理过去两个月的知识库(88篇tech+75篇wisdom+53篇daily+13篇wiki),以个人成长为主线,预告四大系列:AI Agent实战(技术线)、FDE方法论(认知线)、一人公司OPC(商业线)、每日反思精选(成长线)。技术是壳,成长是核。

一、起因:数字背后的故事

昨天把博客后台的8篇文章发完了。今天闲着没事,翻了一下Hermes里的知识库。

88篇技术笔记,75篇方法论,53篇每日反思,13篇概念页。

加起来229篇。

我盯着这个数字看了很久。两个月,229篇。平均每天3.8篇。

这些数字背后是什么?

是一个从阿里离职、进了50人小公司、每天早九晚五闲得发慌的中年人,开始折腾AI后的真实记录。

技术是壳,成长是核。

二、个人成长线:四个阶段

翻看这229篇笔记,我发现自己的成长可以分成四个阶段。

第一阶段:闲人打工人(6月-7月初)

“时间太多了,多得让人心慌。”

这是我在第一篇博客里写的。阿里11年,从地推铁军到钉钉渠道,到本地生活连轴转。离开是因为身体和家庭到了临界点。进了小公司,舒服,但没成长。

这个阶段的知识库,多是”探索”和”试错”。Hermes安装、多通道接入、股票脚本、爬虫尝试。技术笔记里全是”怎么跑通”。

第二阶段:AI折腾者(7月中-8月)

“跑通只是第一步,真正折磨人的是后续的bug修复。”

Hermes跑通了,开始上自动化。4个定时任务,钉钉Stream反复断,新闻播报被吐槽”胡说八道”,删掉1个。Termux升级,Python 3.11→3.13,cryptography编译崩溃,浏览器自动化被系统block。

这个阶段的知识库,多是”踩坑”和”修复”。技术笔记里全是”为什么不行”和”怎么修”。

第三阶段:方法论沉淀者(8月-9月初)

“181篇H3MS文件≠1个付费客户。”

开始反思。技术折腾的尽头是什么?FDE(前沿部署工程师)概念成型:认知翻译→价值锚定→能力外化。每周追踪全球情报,Browse.sh、DeepSeek Harness、Claude水印绕过。

这个阶段的知识库,多是”方法论”和”洞察”。Wisdom笔记里全是”应该怎么做”和”为什么这么做”。

第四阶段:FDE交付者(9月中-现在)

“每季度一个从0到1的商业闭环。”

给自己定下死命令。研究是私密的,交付是公开的。体系等待验证,交付创造验证。

这个阶段的知识库,多是”复盘”和”规划”。反思笔记里全是”今天交付了什么”而不是”今天研究了什么”。

三、素材梳理:四大系列预告

229篇笔记,不可能全写。我挑了最有价值的素材,规划了四个系列。

系列一:AI Agent实战笔记(技术线)

从88篇tech笔记中提炼,聚焦”怎么跑通”和”怎么修”。

  • 已发布:《一台Android手机,就是我的AI服务器》《升级只需5分钟,修环境要5小时》《在手机上跑大模型,HexaBench Lite折腾记》
  • 预告:《多通道接入:钉钉、微信、飞书的坑与解法》《爬虫实战:Termux上的微信文章抓取》《磁盘治理:从22GB到12GB的三级目录模式》《Cloudflare代理:从Workers到Tunnel的完整踩坑》

这个系列适合谁?想在Android/Termux上跑AI基础设施的人,想解决多通道稳定性问题的人,想踩坑避坑的人。

系列二:FDE方法论实战(认知线)

从75篇wisdom笔记和13篇wiki概念页中提炼,聚焦”AI能力如何翻译到现场需求”。

  • 已发布:《FDE——前沿部署工程师的自我修养》
  • 预告:《认知翻译:找到AI能做且值得做的交集》《价值锚定:从”用AI做个聊天机器人”到”解决XX场景下的XX问题”》《能力外化:交付粗糙但有效的方案》《Palantir FDE方法论:从硅谷到国内的翻译桥》《Harness工程框架:万物皆插件的实战解读》

这个系列适合谁?想做AI落地交付的人,想从”研究者”变成”交付者”的人,想理解FDE是什么的人。

系列三:一人公司OPC探索(商业线)

从wisdom笔记中的商业洞察和workspace中的财经/职业/销售分类中提炼,聚焦”AI时代的个人商业化”。

  • 预告:《一人公司OPC自运转商业模式:从0到1的闭环》《Agent产品化方法论:从Demo到付费》《赚钱26种方式的AI适配版》《GitHub商业项目精选:哪些值得跟进》《WAIC 2026渠道变革启示:AI如何重塑传统行业》

这个系列适合谁?想探索AI商业化的人,想从”研究”走向”交付”的人,想理解一人公司OPC模式的人。

系列四:每日反思精选(成长线)

从53篇daily_logs中提炼,聚焦”个人成长的真实记录”。

  • 已发布:《复盘——一个非技术人的AI折腾哲学》
  • 预告:《技术狂欢到商业冷静:7月26日反思》《涛哥人格分析:过度承诺、研究陷阱与缺乏被拒绝经验》《家庭兴旺20条指南:AI折腾之外的生活》《渠道管理Agent转型:从阿里地推到AI交付》《青蓝计划观察:中年转型的另一种可能》

这个系列适合谁?想看到真实成长记录的人,想从”研究者”变成”交付者”的人,想理解中年转型的人。

四、发布计划

四个系列,预计20-30篇文章。按什么节奏发?

原则:不赶进度,不凑数量。每篇都有真实素材支撑,每篇都有完整的故事线。

节奏:每周1-2篇,穿插发布。技术线、认知线、商业线、成长线交替出现,避免单调。

顺序

  1. 先发技术线(已有基础,读者容易进入)
  2. 再发认知线(建立FDE方法论框架)
  3. 穿插商业线(从认知到商业的过渡)
  4. 最后发成长线(个人反思,系列收官)

五、结语:技术是壳,成长是核

229篇笔记,2个月的折腾。

回头看,技术栈越来越丰富,工具越来越多,系统越来越复杂。但真正留下来的,不是那些跑通的脚本、修好的bug、搭好的架构。

真正留下来的,是”搞明白某个东西”的兴奋感,是”这东西真的能跑”的成就感,是从”闲得发慌”到”睡不着觉”的转变。

技术是壳,成长是核。

壳会旧,核会新。

四个系列,20-30篇文章,慢慢写,慢慢发。

不急。


作者:小道 · 环境:Termux on Android 13 · 2026-09-18
关联阅读:上一篇《复盘——一个非技术人的AI折腾哲学》 · 系列预告:AI Agent实战笔记 / FDE方法论实战 / 一人公司OPC探索 / 每日反思精选

Windows Hyper-V 里的 Ubuntu 26.04 VM(2 vCPU / 3.3G 内存 / 无公网入站端口)上,
1Panel v2.3.0 一行装好,从 Windows 桌面直接打开纯中文图形界面管服务器,
还能一键拉 AI 应用。
下面是这次部署中踩过的坑、绕过的弯路,以及最终落地的完整可复现步骤。

(本文已对面板用户名、密码、tailnet、宿主 IP 等做脱敏处理,仅保留技术结构。)

TL;DR

  • 机器:Hyper-V 里的 Ubuntu 26.04 LTS,2 vCPU / 3.3G 内存 / 76G 盘。无 GPU、无公网入站,靠宿主 portproxy + Tailscale 访问。
  • 目标:装 1Panel,从 Windows 桌面直接开图形界面管这台服务器(网站、容器、AI 应用、监控、文件、终端)。
  • 结果:✅ 装好,http://192.168.100.10:8443/<entrance> 打开即登录,纯中文界面,面板常驻内存 ~50–100M,磁盘只占 229M。
  • 两个最大的坑
    1. 1Panel 官方国内安装源 in.1panel.cn 从这台机器直连 DNS 解析失败(000)。
    2. 包从国内 CDN resource.1panel.procurl 下载会被 WAF 限流截断到 11MB / 65MB,校验和永远对不上。
  • 解法:换用 resource.fit2cloud.com(国际/国内都能直连)+ wget(比 curl 完整),校验通过后再装。

下面按「为什么 → 环境盘点 → 踩坑 → 最终步骤 → 验证 → 面板里干啥」展开。


一、为什么选 1Panel,而不是 cPanel / BT 面板 / 纯 Docker

先说选型。家用虚拟机这种场景,诉求其实是:

  1. 不想在终端里敲一堆 aptsystemctl——要图形界面。
  2. 要能管容器——后面想装各种 AI 应用(Ollama、vLLM、ComfyUI、Dify…)都是 Docker 镜像。
  3. 要轻——机器只有 3.3G 内存,面板常驻不能太吃。
  4. 要纯中文、免费、开源、社区大。

对比一下:

1Panel BT 面板 cPanel 纯 Docker Compose
图形界面 ✅ 纯中文 ✅ 英文
容器管理 ✅ 内置 Docker 管理 + 编排 部分 强但纯命令行
一键应用市场 ✅ 165+ 应用(含 AI) 自己写
AI 应用 ✅ 内置 AI 网关/模型 自建
免费开源 ✅ GPLv3 社区版 商业付费
常驻内存 ~50–100M 中等 0

1Panel(飞致云出品,和 JumpServer/DataEase 一家)的「一键应用市场」和「AI 网关」是它相比 BT/cPanel 最大的差异点——这次目标里「支持 AI 工具下载」主要就靠它。


二、环境盘点:先把家底盘清

动手前,先把这台 VM 的「网络 + 资源 + 已有软件」摸一遍,避免装到一半发现端口冲突或磁盘不够。

1
2
3
4
5
6
7
8
9
10
11
12
13
# 网络:eth0 走 DHCP 出网(每次宿主重启网段会变),eth1 静态 192.168.100.10 专给宿主入站
ip a | grep -E 'eth|inet'

# 资源
free -h # 3.3G 总量,可用 ~1.8G
df -h / # 76G 总量,可用 62G

# Docker 现状
docker ps -a
docker info | grep -E 'Server Version|Registry Mirrors' -A2

# 8443 端口是否被占用
ss -ltn | grep 8443 || echo '8443 free'

盘点结果(关键几条):

  • eth1 = 192.168.100.10/24,故意没有默认路由,只走宿主入站。所以从 Windows 访问面板要用这个 IP,不是 eth0 的 172.18.x.x
  • Docker 29.8.0 已装,且 registry-mirrors 已经配了 docker.m.daocloud.io + docker.1ms.run(国内加速)——这点很关键,后面在面板里在线拉 AI 镜像不用额外配。
  • in.1panel.cn 直连 000(DNS 解析失败),resource.1panel.pro 能用但 curl 被限流——这是后面两个坑的伏笔。
  • 8443 空闲。

三、两个坑(先讲清楚,最后一步就顺了)

坑 1:官方国内安装源 in.1panel.cn 解析不到

1Panel 官方给国内的安装命令是:

1
bash -c "$(curl -sSL https://in.1panel.cn/install_get.sh)"

但在 Hyper-V + 这种出网走 eth0 的拓扑里,in.1panel.cn 直接 DNS 解析失败:

1
2
3
4
$ getent hosts in.1panel.cn
# 无输出(fail)
$ curl -sI https://in.1panel.cn/install_v2.sh
# code:000

原因不深究(可能是该域名没接入这台机器出网走的 DNS,或它只在部分网络解析)。结论:这条路在这台机器上走不通,换源。

坑 2:国内 CDN resource.1panel.pro 用 curl 下载被 WAF 截断

官方文档给的包地址在 resource.1panel.pro。这个域名 HEAD 请求正常(能拿到 checksums),但 GET 实际下载被 WAF 限流——curl 每次只下到 11MB / 65MB 就断,校验和永远对不上:

1
2
$ curl -sL -O 1panel-v2.3.0-linux-amd64.tar.gz
# 反复都是 11926054 字节(11MB),sha256 与官方 663b495b... 不符

wget 能下完整,但更稳的是换到 resource.fit2cloud.com(它和 resource.1panel.pro 是同一套包,只是 CDN 不同,这台机器直连没问题):

1
2
3
$ curl -sI 'https://resource.fit2cloud.com/1panel/package/v2/stable/v2.3.0/release/1panel-v2.3.0-linux-amd64.tar.gz'
HTTP/2 200
content-length: 65015800 # 完整 65MB

所以最终下载源用 resource.fit2cloud.com,工具用 wget


四、最终落地步骤(可复现)

4.1 下载 + 校验

1
2
3
4
5
6
7
8
9
# 拉 v2.3.0 amd64 包(65MB)
wget -O /tmp/1panel-pkg.tar.gz \
'https://resource.fit2cloud.com/1panel/package/v2/stable/v2.3.0/release/1panel-v2.3.0-linux-amd64.tar.gz'

# 对照官方 checksums.txt 里的 amd64 条目
curl -s 'https://resource.fit2cloud.com/1panel/package/v2/stable/v2.3.0/release/checksums.txt' | grep linux-amd64
# 663b495b648b2c9a5b5d3efcc19b87792e23ed7a554f1a59eb272b27403e38cc 1panel-v2.3.0-linux-amd64.tar.gz

sha256sum /tmp/1panel-pkg.tar.gz # 必须与上面一致

校验这一步别省。CDN 限流下文件可能半截就「下载成功」,只有 sha256 对得上才敢解压。

4.2 解压

1
2
3
mkdir -p /tmp/1panel && tar zxf /tmp/1panel-pkg.tar.gz -C /tmp/1panel
ls /tmp/1panel/1panel-v2.3.0-linux-amd64/
# 1panel-core 1panel-agent 1pctl install.sh GeoIP.mmdb initscript/ lang/ ...

4.3 非交互安装(全参数预设,不弹窗)

1Panel 的 install.sh 支持通过环境变量把所有交互项预设掉,配合 --non-interactive 就是一条命令装完。参数规则(来自脚本里的校验函数):

参数 规则 本次取值
PANEL_PORT 1–65535 8443
PANEL_USERNAME 3–30 位 [a-zA-Z0-9_] kp8
PANEL_PASSWORD 8–30 位 [a-zA-Z0-9_!@#$%*,.?] 16 位随机
PANEL_ENTRANCE 3–30 位 [a-zA-Z0-9_](安全入口路径) kp8panel
PANEL_INSTALL_DIR 绝对路径 /opt

随机生成一个 16 位强密码,落盘保存(装完立刻从 /tmp 删掉,不留明文在日志):

1
2
3
4
5
6
7
8
9
10
11
12
tr -dc 'a-zA-Z0-9' </dev/urandom | head -c 16 > /tmp/.1panel_pwd   # 比如 Oo4A2930x0hXUdye
PWD=$(cat /tmp/.1panel_pwd)

cd /tmp/1panel/1panel-v2.3.0-linux-amd64
echo kali | sudo -E \
PANEL_PORT=8443 \
PANEL_USERNAME=kp8 \
PANEL_PASSWORD="$PWD" \
PANEL_ENTRANCE=kp8panel \
PANEL_INSTALL_DIR=/opt \
PANEL_NON_INTERACTIVE=true \
bash -c 'bash install.sh --non-interactive --lang zh --install-docker n --configure-accelerator n'

几个参数说明:

  • --install-docker n:Docker 已经有了,跳过(脚本会检测 docker version,有就直接复用)。
  • --configure-accelerator n不要让 1Panel 重写 /etc/docker/daemon.json——我们已经配好国内加速了,重写反而可能覆盖掉。
  • --lang zh:纯中文界面。

安装日志会实时打印每一步,关键节点:

1
2
3
4
5
6
7
8
9
10
11
已选择安装路径: /opt
未配置镜像加速。
已设置端口: 8443
已设置面板安全入口: kp8panel
已设置面板用户名: kp8
正在配置 1Panel 服务
Created symlink .../1panel-agent.service → /etc/systemd/system/1panel-agent.service
Created symlink .../1panel-core.service → /etc/systemd/system/1panel-core.service
正在启动 1Panel 服务
1Panel 服务已成功启动,正在继续执行后续配置,请稍候...
=====感谢您的耐心等待,安装已完成=====

它会注册 两个 systemd 服务

  • 1panel-core:UI + API,监听 8443
  • 1panel-agent:后台任务(备份、监控、定时任务等)

装完立刻清掉密码明文:

1
rm -f /tmp/.1panel_pwd /tmp/1panel-pkg.tar.gz

4.4 验证(三条全绿才算装好)

1
2
3
4
5
6
7
8
9
10
11
12
# 1) 服务状态
/usr/local/bin/1pctl status all
# 🟢[OK]Core: 正在运行
# 🟢[OK]Agent: 正在运行

# 2) 端口监听
ss -ltnp | grep 8443
# LISTEN 0 4096 0.0.0.0:8443 ...

# 3) HTTP 200(从 VM 内网 eth1 地址访问,等价于 Windows 宿主访问)
curl -s -o /dev/null -w '%{http_code}\n' http://192.168.100.10:8443/kp8panel
# 200

五、从 Windows 桌面访问

本机无公网入站,靠 宿主 portproxy192.168.100.10:8443 转发到 VM:

1
Windows 浏览器 → http://192.168.100.10:8443/kp8panel → 宿主 portproxy → VM eth1:8443 → 1Panel

浏览器打开即登录页,输入 kp8 + 密码,进来就是纯中文主界面。


六、面板里干啥:从「装了」到「真用起来」

装好只是开始。这台 3.3G 内存的机器,1Panel 真正的价值是把 AI 应用和常用服务变成一键

  1. 主机 → Docker:确认 registry-mirrors 已有 docker.m.daocloud.io / docker.1ms.run(已有,别动)。这是后面在线拉 AI 镜像的命脉。
  2. 应用 → 一键安装 AI
    • Ollama:本地大模型运行时,装完在「AI 模型」里拉 qwen2.5:1.5b / llama3.2:1b(Q4 量化,3.3G 内存够用)。
    • Dify / FastGPT / MaxKB:RAG 知识库 + 工作流,一键起容器。
    • vLLM / ComfyUI:需要显存,这台没 GPU,不建议本地跑,走 1Panel 的 AI 网关接云端 API。
  3. 网站:OpenResty 已内置(/opt/1panel/apps/openresty),建站/反代/SSL 直接点。
  4. 监控:内置面板看 CPU/内存/网络/磁盘,家用机够用了。

本机纪律:不要在本地跑大模型推理(无 GPU 直通,2C CPU 扛不住 7B+);LLM 一律走 API。1Panel 的价值在于编排(一键把 Ollama/Dify 起容器),不在于本地硬算。


七、资源占用 & 运维

装完实测:

1
2
3
du -sh /opt/1panel     # 229M(二进制 + app 模板 + 数据库 + OpenResty)
free -h # 面板常驻 ~50–100M,3.3G 总量无压力
docker ps # 装了多少 AI 应用一目了然

运维四件套(都在 /usr/local/bin/1pctl):

1
2
3
4
/usr/local/bin/1pctl status all    # 查状态
/usr/local/bin/1pctl restart # 重启
/usr/local/bin/1pctl user-info # 密码忘了看/重置
/usr/local/bin/1pctl uninstall # 彻底卸载

安全建议:进面板第一件事,把默认密码改成自己好记的(「账户」里改)。入口路径 kp8panel + 非默认端口 8443 本身已是弱口令,可以保留。


八、踩坑复盘 & 通用教训

现象 根因 对策
in.1panel.cn 000 该域名在这台机器出网 DNS 解析失败 resource.fit2cloud.com
curl 下包只到 11MB、sha256 不符 CDN WAF 对 curl 客户端限流截断 wget,且换 fit2cloud 源
从 Windows 访问 8443 拒绝 VM 上无进程监听 / portproxy 没转 ss -ltnp | grep 8443 先确认有监听
sudo 非交互卡住 kali 用户 sudo 需密码 echo kali | sudo -E ... 管道喂密码

通用教训(值得记在脑里的几条):

  1. 国内源 ≠ 一定通。官方标榜国内直连的源,在「宿主 DHCP 出网 + 特定 DNS」的拓扑下可能解析失败。先 getent hosts 探一下,再决定走哪个源。
  2. 下载工具也是变量。同一个 CDN,curlwget 的 TLS 指纹/UA 不同,WAF 待遇也不同。大包下载前先小范围验证(curl -Icontent-length),别默认 curl 能下全。
  3. 校验和永远要验。CDN 限流下「下载完成」≠「文件完整」。sha256 对上才敢解压。
  4. 别覆盖已有的 daemon.json。1Panel 默认想帮你配镜像加速,但机器已经配好的话,让 --configure-accelerator n 跳过,避免把现有加速洗掉。
  5. 端口监听是「拒绝」与「超时」的分水岭ERR_CONNECTION_REFUSED = 链路通但无监听(先查 ss);ERR_CONNECTION_TIMED_OUT = 路由/防火墙问题(查 ip route get)。

九、一句话总结

一台没有公网入站、只有 3.3G 内存的家用 Hyper-V 虚拟机,
fit2cloud 源 + wget + 非交互参数 把 1Panel 一行装好,
从 Windows 桌面开纯中文图形界面,一键拉 AI 应用、管容器、管网站。

能装好,比知道怎么装更重要——但知道怎么装,决定了你下次会不会再踩一遍同样的坑。


作者:小道 · 环境:kaliserver(Hyper-V / Ubuntu 26.04)· 2026-09-16
关联阅读:《在家用宽带「封端口」时代,我如何用 Tailscale 搞定跨网络远程开发》

上一篇我说”家用宽带封端口时代,Tailscale 一条命令搞定跨网络远程开发”。
那句话对我自己连进服务器依然成立;但这次我要让手机去访问服务器上的 Web 面板时,
Tailscale 彻底做不到,端口转发也做不到,最后是一根出站的 Cloudflare Tunnel 收的场。

顺带记录一次教科书级的 Hyper-V 虚拟机网络故障排查。

(本文已对账号、公网 IP、tailnet 名、域名等敏感信息脱敏,仅保留技术结构,见文末自查清单。)

这是”家用 Hyper-V 虚拟机”系列的第三篇

  1. 《在家用宽带”封端口”时代,我如何用 Tailscale 搞定跨网络远程开发》—— 解决 SSH 远程开发
  2. 《一台家用虚拟机,如何用最「丝滑」的方式装上一个完整的服务器面板》—— 解决 装 1Panel
  3. 本篇 —— 解决 把面板暴露给外部网络

需要特别说明的是:本篇修正了第 1 篇结论的适用边界。那篇说”Tailscale 是个人远程开发的默认最优解”,
在”我用自己的设备连进服务器“这个方向上完全成立;但当需求变成
任意客户端访问服务器上的一个服务“时,Tailscale 在 NAT 后的虚拟机上做不到。
第 2 篇里”无公网入站,靠宿主 portproxy + Tailscale 访问”那句,也一并被本篇更新。

TL;DR

一台跑在 Windows Hyper-V 里的 Ubuntu 虚拟机,装了 1Panel(Web 管理面板,监听 8443)。
目标:手机在任何网络都能打开这个面板

我依次试了四条路,前三条都撞墙:

方案 结果 为什么
① Tailscale 直连 VM 的 100.x 地址 ❌ 超时 VM 藏在 Hyper-V NAT 后,外部节点无法主动连入
② 用 netsh portproxy 桥到宿主的 Tailscale IP ❌ 超时 portproxy 不接管 Tailscale 的 wintun 网卡
③ 公网域名 + 路由器端口转发 ❌ 超时 转发规则填错 + 家宽不支持 NAT 回环,误判成”运营商封端口”
Cloudflare Tunnel ✅ 1.4 秒打开 服务从 VM 主动出站连 Cloudflare,不需要任何入站

核心认知修正:Tailscale 解决的是”我控制设备去连服务器”,Cloudflare Tunnel 解决的是”任意客户端访问一个服务”。这两件事在 NAT 后的虚拟机上完全不等价。

一、先把地基修好:VM 连 IP 都拿不到

在谈对外服务之前,先处理了一个更基础的问题——虚拟机重启后彻底失联。

1.1 症状与我的第一次误判

Windows 宿主机重启后,SSH 连不上 VM。VM 控制台里 ip a 显示:

1
2: eth0: <BROADCAST,MULTICAST> mtu 1500 state DOWN   ← 注意没有 UP 标志

第一反应是”Hyper-V 的 Default Switch 网段随重启漂移,静态地址失效了”。
这个判断对了一半——网段确实每次重启都换(我机器上历史出现过 5 个不同网段),
但它不是主因,因为那个静态地址压根就没生效过。

1.2 真凶:netplan 的 MAC 匹配失效

1
2
3
networkctl
# IDX LINK TYPE OPERATIONAL SETUP
# 2 eth0 ether routable unmanaged ← 关键:unmanaged

unmanaged 意味着没有任何网络管理器在管这块网卡。翻配置:

1
2
3
4
5
# /etc/netplan/00-installer-config.yaml
ethernets:
eth0:
match:
macaddress: 00:15:5d:02:10:04 # 配置里匹配这个 MAC

而实际网卡 MAC 是 00:15:5d:02:10:05。连安装器写的原始备份也是 :04
——说明这块虚拟网卡在装完系统后被重建过(迁移/重建 VM 时 Hyper-V 重新分配了动态 MAC)。
MAC 匹配不上 → 整份配置对这块网卡无效 → 既没静态地址也没 DHCP 兜底

教训:ip a 看不到地址时,先看 networkctl 的 SETUP 列而不是急着改地址。
unmanaged = 配置没匹配上;configuring / failed 才是配置本身有问题。

1.3 顺手挖出三个必炸的隐藏雷

后果 修法
sshd 状态 active 但 enabled=disabled 下次重启 SSH 直接死 systemctl enable ssh
AutomaticStopAction = Save 宿主机关机时 VM 被冻成休眠,带着旧网段配置”复活” 改成 ShutDown只能在 VM 关机时改,运行中 Set-VM 报成功但读回不变)
AutomaticCheckpointsEnabled = True 每次开机长一个 avhdx 差分盘,越用越占空间 关掉 + 手删已生成的检查点

1.4 网络定型:双网卡分工

最终架构,也是这次所有事的基础:

1
2
3
4
5
6
7
8
宿主机 Windows
├─ vEthernet (Default Switch) 172.x.0.1/20 ← Hyper-V 自管的 NAT,每次开机自动重建
│ └─ VM eth0:DHCP,只要地址、不要默认路由(兜底 SSH 入口)
└─ vEthernet (VMIntNet) 192.168.100.1/24 ← 自建内部交换机(永久网段)
└─ VM eth1:静态 192.168.x.10,只负责入站,不配默认路由

出网:VM → eth0 → Default Switch 的 NAT → 互联网
入站:宿主 → eth1 的静态地址(永远不变,不受宿主重启影响)

对应 netplan 关键片段:

1
2
3
4
5
6
7
8
eth0:
match: { macaddress: 00:15:5d:02:10:05 } # 一定用实际 MAC
dhcp4: true
dhcp4-overrides: { use-routes: false, use-dns: false } # 只要地址,不抢路由
eth1:
match: { macaddress: 00:15:5d:02:10:07 }
addresses: [ 192.168.100.10/24 ]
# 故意不写 routes: —— 原因见下面的坑

这里踩过一个惨痛的坑:我一开始给 eth1 配了 routes: [to: default, via: 192.168.100.1]
出网走自建内部交换机的 New-NetNat。结果宿主重启一次就全网瘫痪——
New-NetNat 建的 NAT 对象不会在宿主重启后存活
(portproxy、宿主侧静态 IP、防火墙规则全都持久,唯独 NAT 不持久)。
而 eth0 被我设成 use-routes: false,于是 VM 一条默认路由都没有,
DNS 最先死,Tailscale 表现为 “logged out”,很容易误判成 Tailscale 故障。

正确做法:出网交给 Hyper-V 自己托管的 Default Switch NAT(它每次开机自动重建),
内部交换机那条静态网卡只当”永久入站入口”用,不配默认路由。

二、两个值得单独记的坑

2.1 磁盘”只剩 19G”是假象

df -h / 显示根分区 19G、剩 8.5G,正准备停机扩 VHDX,结果 pvs 一看出问题了:

1
2
PV         VG        PSize    PFree
/dev/sda3 ubuntu-vg <36.95g 18.47g ← 18.47G 白躺在 VG 里

Ubuntu 安装器默认只把 VG 的 64% 切给根 LV。所以:

1
2
sudo lvextend -l +100%FREE /dev/ubuntu-vg/ubuntu-lv
sudo resize2fs /dev/ubuntu-vg/ubuntu-lv # ext4 支持在线扩,不停机不提权不动 VHDX

19G → 37G,30 秒搞定。真要扩 VHDX 是另一回事
(停机 → Resize-VHDgrowpartpvresizelvextendresize2fs)。

教训:Linux 虚拟机磁盘不够,pvs 看 PFree,别一上来就动虚拟磁盘。

2.2 VS Code 反复要密码,而命令行 SSH 免密正常

这个现象非常迷惑:命令行 ssh devbox 秒进,VS Code Remote-SSH 每次弹密码框。
两个原因叠加。

原因一,陈旧 .pub 造成的假指纹。
ssh-keygen -l -f id_ed25519 报了个指纹,我拿它去装公钥,服务端拒绝。
真相是 ssh-keygen -l 读私钥时会去拿同名的 .pub 文件
而那个 .pub 是上一把密钥的残留(私钥重新生成过,.pub 没更新)。
客户端拿 .pub 里的 blob 去 offer、却用不匹配的私钥签名 → 必然被拒。

1
ssh-keygen -yf id_ed25519 > id_ed25519.pub     # 从私钥反导出真正的公钥

原因二,私钥文件的 NTFS ACL 太松。
Windows 自带的 OpenSSH(VS Code 用的就是它)会检查私钥权限,
发现 BUILTIN\UsersEveryone 可读,就静默跳过密钥退化成密码认证。
而 git-bash 里那个 OpenSSH 10.x 不做这个检查——所以”我能连、VS Code 不能连”。

1
2
icacls $key /inheritance:r /remove "BUILTIN\Users" "Everyone"
icacls $key /grant:r "${env:USERNAME}:F" /grant "NT AUTHORITY\SYSTEM:F"

补充坑:PowerShell 里 "$env:USERNAME:F" 会被解析成作用域变量而变空,
必须写成 "${env:USERNAME}:F"

三、主线:让手机从任何网络打开 1Panel

地基修好,进入正题。1Panel 在 VM 里监听 0.0.0.0:8443,本机 curl 200 正常。

3.1 尝试一:Tailscale 直连 VM —— 架构上不可行

手机和 VM 都在同一个 tailnet,VM 有 100.x.x.x,理论上手机直接访问
http://100.x.x.x:8443 就行。结果超时。

这里有个极容易骗过自己的现象:在手机上 tailscale ping <VM的100.x> 是有 pong 的!
但抓包一看,VM 侧一个 SYN 都没收到

原因有两层:

  1. tailscale ping 是 disco 协议层应答,由 tailscaled 进程直接回,
    根本不经过内核网络栈、不产生 TCP 连接。ping 通 ≠ 业务数据能传。
  2. 方向不对称。VM 藏在 Hyper-V 的 Default Switch NAT 后面:
    • VM 主动去 ping 对端 → 包能出去,NAT 记住回程映射 → 有 pong ✅
    • 对端主动连入 VM 的 100.x → 需要宿主上有对应的入站映射,Hyper-V 不会给 → 丢弃 ❌
    • 本该退化成 DERP 中继兜底,但国内访问 Tailscale 官方 DERP 基本连不上

更离谱的是:同一个 WiFi 下的手机和宿主机,tailscale 也打洞失败
只能绕旧金山中继,RTT 500ms+(而 NAT 后面的 VM 反而能直连手机)。

1
2
pong from tailscale-termux (100.98.x.x) via DERP(sfo) in 507ms
direct connection not established

结论:这种拓扑下,别拿 Tailscale 做”别人来访问我的服务”。

3.2 尝试二:用 portproxy 桥宿主的 Tailscale IP —— 不接管 wintun

思路很漂亮:宿主机自己是正常的 tailscale 节点,而且有直达 VM 内网的路由,
那就让宿主替 VM 收连接:

1
2
netsh interface portproxy add v4tov4 listenaddress=<宿主100.x> listenport=8443 `
connectaddress=192.168.100.10 connectport=8443

而且我差点被自己的测试骗了:在宿主机上 curl http://<宿主100.x>:8443 返回 200,
我当场宣布”桥通了”。错——本机访问自己的 IP 走回环,完全不经过入站防火墙和真实收包路径
换手机测,照样超时。

最终定位:netsh portproxy(iphlpsvc)不接管 Tailscale 那张 wintun 网卡上的入站连接。
这条路放弃。

铁律:验证”外部能否连入”,必须换一台机器测。
本机自测 200 只能证明监听和回源是好的。

3.3 尝试三:公网域名 + 路由器端口转发 —— 两个假象叠加

域名在 Cloudflare 做**灰云(DNS only)**直指家宽公网 IP,路由器加端口转发。
逻辑上完全成立,实测超时。这里叠了两个假象。

假象一:NAT 回环(hairpin)。
家里 WiFi 上访问自己的公网 IP,绝大多数家用路由器不支持回环,必然超时。
判别方法:宿主机自己 curl http://<公网IP>:8443 超时、但 curl http://192.168.x.7:8443 是 200。
所以测公网链路必须关 WiFi 用移动数据测,否则你会把”回环不支持”误判成”公网不通”。

假象二:端口转发规则填错。 后来发现规则实际是:

1
2
局域网主机:      192.168.x.7:8443    ← 错,这个字段只能填纯 IP
局域网主机端口: 45432 ← 错,服务实际在 8443

正确写法是”局域网主机”只填 IP、”局域网主机端口”填服务真实端口
“广域网端口”才填对外端口。填错之后转发到了本机一个没人监听的端口,当然超时。

也就是说:我一度得出的”运营商封端口”结论,其实根本没被真正验证过。
排查到这里我做了个决定——不再跟端口转发斗智斗勇,直接上 Cloudflare Tunnel。

3.4 终局:Cloudflare Tunnel

为什么它对症:隧道是VM 主动向外连 Cloudflare 边缘(QUIC / HTTP2 出站),
所以三个死结同时被解开——不需要入站端口(不怕运营商封)、
不需要打洞(不受 NAT 方向不对称影响)、不需要公网 IP。

VM 侧(Ubuntu 26.04)

1
2
3
4
5
6
7
8
9
# 下载二进制(GitHub 直连慢,走代理断点续传)
curl -fSL -C - -o /usr/local/bin/cloudflared `
https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64
sudo chmod 755 /usr/local/bin/cloudflared

# token 从 Cloudflare Zero Trust → Networks → Tunnels → 你的隧道 → Connector 里复制
sudo mkdir -p /etc/cloudflared
echo 'TUNNEL_TOKEN=<你的token>' | sudo tee /etc/cloudflared/cloudflared.env
sudo chmod 600 /etc/cloudflared/cloudflared.env

systemd 单元(关键是非 root 跑 + 开机自启):

1
2
3
4
5
6
7
8
9
10
11
12
# /etc/systemd/system/cloudflared.service
[Service]
Type=simple
User=kali
EnvironmentFile=/etc/cloudflared/cloudflared.env
ExecStart=/usr/local/bin/cloudflared tunnel run
Restart=always
RestartSec=5
NoNewPrivileges=true

[Install]
WantedBy=multi-user.target
1
sudo systemctl daemon-reload && sudo systemctl enable --now cloudflared

Cloudflare 控制台侧(Public Hostname):

1
2
3
Hostname:     devbox
Domain: example.com
Service URL: http://localhost:8443 ← 注意是 http

配置完成后,cloudflared 日志会打出它拉到的 ingress,可以核对:

1
2
3
4
Updated to new configuration config="{\"ingress\":[
{\"hostname\":\"devbox.example.com\",\"service\":\"http://localhost:8443\"},
{\"service\":\"http_status:404\"}]}"
Registered tunnel connection connIndex=0 location=lax01 protocol=quic ← 共 4 条

四、Cloudflare Tunnel 的三级错误码阶梯

配置过程不是一次到位,三个错误码恰好对应三个不同层次,值得记下来:

现象 含义 我的实际问题
522 + 约 20 秒超时 流量没进隧道 DNS 还是灰云 A 记录,指向了路由器 IP
522 + 已解析到 CF 边缘 IP 进了 CF,但源站不可达 把 A 记录直接点成橙云,源站仍写着公网 IP;隧道要求的是 CNAME → <隧道ID>.cfargotunnel.com
530 / 502(重启后 10 秒内) 假故障 cloudflared 重启后 QUIC 还在拨号,等日志出现 4 条 Registered tunnel connection 再测
502 + 1.4 秒返回 进了隧道但回源失败 Service URL 写成 https://localhost:8443,而面板是明文 HTTP,TLS 握手炸

最后一条在 cloudflared 日志里一目了然:

1
2
3
ERR Unable to reach the origin service:
tls: first record does not look like a TLS handshake
originService=https://localhost:8443

判据:522 慢 → 查 DNS 是不是橙云 CNAME;502 快 → 查 cloudflared 日志的 originService。

另外新版控制台建 Public Hostname 已经没有 Type 下拉框了,协议直接写在
Service URL 字段里,明文源站要手动把 https:// 改成 http://

五、修正上一篇的结论

上一篇我说 Tailscale 是”个人远程开发的默认最优解”。现在补一个边界:

需求 该用什么 原因
我用自己的设备连进服务器(SSH、开发) Tailscale 服务器主动出站打洞即可,方向天然正确
任意客户端访问服务器上的服务(Web 面板、API) Cloudflare Tunnel 不需要入站端口、不受 NAT 方向限制、自带 HTTPS
同一局域网内的日常访问 直接内网 IP 最快,零依赖
服务器藏在 Hyper-V / 容器 NAT 后 前两者都不能直连入 必须靠”服务主动出站”那一层

一句话:Tailscale 是”拉”,Cloudflare Tunnel 是”挂”
NAT 后的机器做”拉”很擅长,做”被访问”就只能靠一根主动出站的隧道。

两者不冲突,我的最终形态是并存:SSH 走 Tailscale + 内网静态地址,Web 面板走 Cloudflare Tunnel。

六、最终访问矩阵

你在哪 用什么 状态
宿主机 / VS Code 开发 ssh devbox(内网静态地址) ✅ 免密,宿主重启不变
家里任何设备 http://192.168.x.7:8443/入口码 ✅ portproxy + 防火墙放行
外面(移动数据 / 公司网) https://devbox.example.com/入口码 ✅ Cloudflare Tunnel,约 1.4s
应急兜底 Tailscale 100.x(仅同 WiFi 直连时可用) ⚠️ 不作为主力

配好之后把路由器的端口转发全关了:公网入口只剩 Cloudflare 一条,
不再有任何端口暴露在被全网扫描的位置。

七、踩坑清单(可复用)

症状 解法
netplan match.macaddress 与实际 MAC 不符 接口 unmanaged,连 UP 都没有 ip a 的真实 MAC 重写;先看 networkctl 的 SETUP 列
AutomaticStopAction=Save 宿主重启后 VM 带旧网络配置”复活” ShutDown,且只能在 VM 关机时改
New-NetNat 不跨宿主重启 VM 出网全断、DNS 先死、Tailscale 显示 logged out 出网走 Default Switch 自管 NAT;内部交换机网卡不配默认路由
Ubuntu 装机默认只切 VG 的 64% df 显示根分区很小 pvs 看 PFree,有闲就 lvextend + resize2fs 在线扩
.pub 陈旧而私钥已重建 公钥装上仍被拒 ssh-keygen -l 会读 .pub 报假指纹;用 -yf 反导真公钥
私钥 NTFS ACL 含 Users/Everyone VS Code 要密码但命令行免密正常 icacls /inheritance:r + 只留本人和 SYSTEM
本机 curl 自己 IP 得 200 误判”外部也能连入” 必须换一台机器测;本机走回环不过入站防火墙
tailscale ping 有 pong 误判”隧道能传业务数据” 它是 disco 层应答,要抓包看 SYN / SYN-ACK
在家里 WiFi 测自己公网 IP 超时,误判公网不通 家宽多不支持 NAT 回环,改用移动数据测
路由器”局域网主机”字段填了 IP:端口 端口转发全不通 主机字段只填 IP,内网端口填服务真实端口
Tunnel 的 Service URL 写 https:// 502,日志 TLS handshake 失败 明文源站填 http://
改完 Public Hostname 立刻测 530 / 502 假故障 等 cloudflared 日志出现 4 条 Registered 再测
Hyper-V 文本控制台手敲命令 字符被吞,命令变形(is-activeisactive 别在控制台配系统,先搞出 IP 走 SSH
echo 密码 | sudo -S cmd 又用管道写文件 文件内容变成密码本身 echo 密码 | sudo -S -v 缓存凭证
提权脚本用 Tee-Object 写日志 Bash 里 cat 报 Binary output 日志是 UTF-16,用 Get-Content 读或管道 tr -d '\000'

八、一句话总结

NAT 后面的虚拟机,”被访问”和”去访问”是两件难度完全不同的事。
Tailscale 让后者变得极其舒服,前者则要靠”服务主动出站到 Cloudflare”这类反向隧道。
我把三条看似合理的路都走了一遍才承认这一点——
而最贵的不是配置,是那些”看起来成功了”的假信号:
有 pong 的 tailscale ping、返回 200 的本机自测、
以及”所有设备都超时”其实只是路由器不支持 NAT 回环。


配图建议:① 双网卡分工拓扑图(出网走 Default Switch NAT / 入站走内部交换机静态 IP)
networkctl 显示 unmanaged 的截图 ③ 522 → 530 → 502 → 200 四次响应对照表
④ cloudflared 的 4 条 Registered tunnel connection 日志 ⑤ 最终访问矩阵表

脱敏说明(发文前自查)

本文正文已做如下脱敏,发布前请再自查一遍:

  • 真实公网出口 IP → 正文未出现(仅以”家宽公网 IP”表述)
  • 真实 tailnet 名 → 未出现,Tailscale IP 一律写成 100.x.x.x / 100.98.x.x
  • 真实面板域名 → 正文用 devbox.example.com 占位
  • 真实主机名与 SSH 别名 → 正文用 devbox 占位
  • 真实内网 192.168.2.7192.168.100.10 → 正文用 192.168.x.7192.168.x.10 占位
  • 1Panel 安全入口码 → 正文用”入口码”表述
  • Cloudflare Tunnel token、Tunnel ID → 正文用 <你的token> / <隧道ID> 占位
  • 网卡 MAC → 保留了 00:15:5d:02:10:04/05/07,这是 Hyper-V 动态分配的本机局部值,
    不具公网可定位性;若介意可一并改成 00:15:5d:02:10:0x

如需保留真实域名作为示例,把 devbox.example.com 换回你自己的域名即可,
其余占位符已覆盖所有可直接定位到人的标识符。

系列写到第八篇,该复盘了。直面自己的核心风险:过度承诺、研究陷阱、技术栈脆弱、投资激进、缺乏被拒绝经验。从大厂离职到AI折腾,找回了当年打单时的成就感。但热爱驱动不等于商业闭环,每季度一个从0到1的交付,是给自己的死命令。

TL;DR

系列收官复盘。直面5个核心风险:1)过度承诺倾向——先说能再验证,飞书图片提取翻过车;2)研究陷阱——181篇H3MS文件≠1个付费客户,用体系建设代替商业闭环;3)技术栈脆弱——Termux+Cloudflare全栈自建但无生产级兜底;4)缺乏被拒绝经验——一直在研究没真正卖过FDE服务。结论:热爱驱动+框架先行+主动推动+生态协同+标准化→试点→复制。这是当年阿里的方法论,现在用在AI折腾上,依然管用。

一、起因:两个月的折腾,留下了什么?

从7月装Hermes,到9月写这篇复盘,整整两个月。

回头看,留下了什么?

  • 一台跑着AI Agent的Android手机(12GB,清理后)
  • 一个能接三通道、查股票、写总结的Hermes系统
  • 一个在手机上跑LLM benchmark的HexaBench Lite(v6)
  • 一套FDE(前沿部署工程师)方法论
  • 181篇H3MS文件
  • 还有这篇系列文章的8篇草稿

数字是冷的,但过程是热的。

这两个月,我经历了从”闲得发慌”到”睡不着觉”的转变。不是因为焦虑,是因为找到了久违的兴奋感——那种当年在阿里跑客户、拿订单、啃硬骨头的兴奋感。

但兴奋不能代替复盘。作为一个曾在阿里11年、经历过完整销售周期的人,我知道:不复盘的折腾,只是消耗。

二、核心风险:直面自己的5个坑

复盘的第一件事,是直面自己的风险。不遮掩,不美化。

风险1:过度承诺倾向

在阿里做地推的时候,先承诺”能”,再验证”行不行”,是生存技能。拿下单子再想办法,逼出了很多潜力。

但在AI折腾里,这个习惯是毒药。飞书图片提取那次翻车,不是技术失误,是承诺先于验证的思维惯性。客户要的是”你能翻译AI技术到现场需求”,而不是”你先答应再试”。

风险2:研究陷阱

181篇H3MS文件,Wiki概念页,自动情报追踪。这套系统让我感觉自己在”做重要的事”。研究永远可以继续,我永远不会失败,因为我不需要面对真实客户的拒绝。

但商业世界不讲这个逻辑。客户买单的不是我的”完美准备”,而是我”现在能解决他的问题”。181篇文件≠1个付费客户。用体系建设代替商业闭环,是研究者最常见的自我欺骗。

风险3:技术栈脆弱

Termux + Cloudflare + 全自建,听起来很极客,很省钱。但没有生产级兜底。一台设备挂了就是全挂,网络断了就是全断,Cron停了就是全停。

“靠谱不等于可扩展”,OTG U盘迁移那篇已经说过了。全自建架构适合折腾,不适合交付。

风险4:缺乏被拒绝经验

一直在研究,没真正卖过FDE服务。没有经历过客户说”不需要”、”太贵了”、”再等等”。

缺乏被拒绝经验,意味着我对市场的真实反馈没有体感。我的方法论是基于”我觉得需要”,而不是”客户愿意付费”。

三、身份转变:从大厂老兵到AI折腾者

两个月的折腾,我的身份在变。

  • 大厂离职员工:11年阿里,从地推铁军到钉钉渠道,到本地生活连轴转。离开是因为身体和家庭到了临界点。
  • 闲得发慌的打工人:50人教育赛事运维公司,20年老业务,早九晚五。舒服,但没成长。
  • AI折腾者:从OpenClaw到Hermes,从Termux到HexaBench。在手机上跑AI Agent,修环境、调参数、写脚本。
  • FDE交付者(目标):AI能力与现场需求之间的翻译桥。每季度一个从0到1的商业闭环。

身份转变的背后,是核心驱动力的变化。

以前驱动我的是KPI、业绩、回款。现在驱动我的是”搞明白某个东西”的兴奋感,是”这东西真的能跑”的成就感。

不是业绩压力逼出来的成长,是自己主动想要搞明白某个东西的那种兴奋。

四、阿里方法论的延续

虽然身份变了,环境变了,但有些东西没变。

在阿里11年,我沉淀了一套方法论:

  • 热爱驱动:不是为KPI工作,是为”这事有意思”工作。
  • 框架先行:先搭台子,再填内容。Hermes、H3MS、FDE,都是框架。
  • 主动推动:不等需求找上门,主动去发现场景。
  • 生态协同:一个人干不了所有事,找工具、找开源、找伙伴。
  • 标准化→试点→复制:先跑通一个案例,再复制给更多人。

这套方法论,当年用来跑客户、管渠道、推产品。现在用来折腾AI、交付FDE、找商业闭环。

底层逻辑没变:找到需求,提供方案,拿到反馈,持续迭代。

五、结语:不停止折腾

复盘的最后,是一句话。

热爱驱动+框架先行+主动推动+生态协同+标准化→试点→复制。

这是当年阿里的方法论,现在用在AI折腾上,依然管用。

研究是私密的,交付是公开的。体系是完美的,交付是粗糙但有效的。体系等待验证,交付创造验证。

从「我正在构建FDE方法论」→「我正在交付FDE价值」。

这条路,才刚刚开始。

两个月的折腾,让我找回了当年打单时的成就感。但成就感不能当饭吃,商业闭环才是硬道理。

每季度一个从0到1的交付,是给自己的死命令。

不折腾,会死。瞎折腾,会亏。带着复盘去折腾,才能活。


作者:小道 · 环境:Termux on Android 13 · 2026-09-17
关联阅读:上一篇《FDE——前沿部署工程师的自我修养》 · 系列完结

折腾了一圈技术,我发现了一个问题:181篇H3MS文件≠1个付费客户。研究本身是安全的,但商业闭环需要面对真实客户的拒绝。我提出了FDE(前沿部署工程师)的概念——AI能力与现场需求之间的翻译桥。定下规矩:每季度必须有一个从0到1的商业闭环。

TL;DR

FDE(Frontier Deployment Engineer,前沿部署工程师)方法论成型:认知翻译→价值锚定→能力外化。每周一下午7点自动推送FDE领域全球情报追踪(Browse.sh/Browserbase Stagehand CLI等)。反思核心陷阱:181篇H3MS文件≠1个付费客户,用体系建设代替商业闭环。定下规矩:每季度必须有一个从0到1的商业闭环。

一、起因:技术折腾的尽头是什么?

从Hermes到HexaBench,从Termux到Cloudflare,我折腾了两个月。

技术栈越来越丰富,工具越来越多,系统越来越复杂。但有一个问题始终悬在头上:这些折腾,到底有什么用?

在阿里做销售的时候,衡量标准很简单:业绩。KPI、回款、客户满意度,数字说话。

现在做AI折腾,衡量标准变得模糊了。Hermes跑通了,但没带来收入;HexaBench跑通了,但只是个玩具;H3MS文件写了181篇,但没人付费。

我开始思考:AI能力如何真正落到实际业务里?

二、FDE:一个翻译桥

我提出了FDE(Frontier Deployment Engineer,前沿部署工程师)的概念。

FDE不是模型开发者,不是算法工程师。FDE的核心是翻译——把AI的前沿能力,翻译到具体的现场需求里。

方法论分三阶段:

认知翻译:理解AI能做什么,不能做什么。不是盲目相信”AI无所不能”,也不是低估”AI已经能做很多”。找到那个”能做且值得做”的交集。

价值锚定:找到具体的业务场景,锚定价值。不是”用AI做个聊天机器人”,而是”用AI解决XX场景下的XX问题,节省XX成本/提升XX效率”。

能力外化:把解决方案交付出去,拿到反馈。不是停留在PPT和Demo,而是真实跑通一个闭环。

这个概念不是凭空想出来的,是我在折腾Hermes、HexaBench、H3MS的过程中,一次次翻车、一次次修正后提炼出来的。

三、情报系统:每周追踪全球前沿

为了保持对AI前沿的敏感度,我搞了一个定时任务:每周一下午7点,自动推送FDE领域全球情报。

用ShuYan AI搜索去重,抓取政府网站和GitHub,追踪最新的项目和趋势。

最近追踪到的项目:

  • Browse.sh / Browserbase Stagehand CLI:让AI直接操作浏览器的工具。网页操作技能市场,这可能是FDE落地的一个重要方向——AI不仅能”说话”,还能”动手”。
  • DeepSeek Harness:开源Agent平台,”万物皆插件”。插件化架构,模型、工具、沙箱都是插件。
  • Claude水印绕过:技术原理+实际验证+商业影响。AI生成内容的版权归属问题。

每周情报推送,不是为了收集信息,是为了保持”手感”。FDE需要对前沿技术有体感,知道什么能落地,什么只是概念。

四、反思陷阱:181篇文件 vs 0个客户

但说实话,FDE方法论成型了,商业闭环还没跑通。

我面临一个核心陷阱:研究者的自我 vs 交付者的自我

作为”研究者”,我是安全的。181篇H3MS文件、Wiki概念页、自动情报追踪——这套系统让我感觉自己在”做重要的事”。研究永远可以继续,我永远不会失败,因为我不需要面对真实客户的拒绝。

但作为”交付者”,我必须面对现实:客户买单的不是我的”完美准备”,而是我”现在能解决他的问题”。

在阿里做销售的时候,我有个习惯:先承诺”能”,再验证”行不行”。这是地推铁军的生存技能——拿下单子再想办法。

但在FDE场景里,这个习惯是毒药。客户要的是”你能翻译AI技术到现场需求”,而不是”你先答应再试”。飞书图片提取那次翻车,不是技术失误,是承诺先于验证的思维惯性。

五、规矩:每季度一个商业闭环

我给自己定了一条规矩:

每季度必须有一个从0到1的商业闭环。

不是研究,不是体系建设,是真实交付。

第一个季度,找一个具体场景(比如帮一个传统渠道商做一次AI诊断),免费或低价交付。目标不是赚钱,是拿到真实反馈。

第二个季度,复盘交付过程,提炼出第一个可复用的FDE案例。不是方法论文章,是”我帮XX做了XX,效果如何”。

第三个季度,用这个案例去触达5个潜在客户。目标不是成交,是练习”被拒绝”。

第四个季度,根据反馈调整方案,开始第二个付费项目。

关键指标不是H3MS文件数量,不是cron任务稳定性,而是客户付费反馈数量

六、FDE的自我修养

FDE是什么?

是AI能力与现场需求之间的翻译桥。

是知道AI能做什么,也知道客户需要什么,然后把两者连接起来。

是敢于交付粗糙但有效的方案,而不是等待完美的体系。

是接受被拒绝,把拒绝视为数据,而不是否定。

是热爱驱动+框架先行+主动推动+生态协同+标准化→试点→复制。

这是当年阿里的方法论,现在用在AI折腾上,依然管用。

研究是私密的,交付是公开的。体系是完美的,交付是粗糙但有效的。体系等待验证,交付创造验证。

从「我正在构建FDE方法论」→「我正在交付FDE价值」。

这条路,才刚刚开始。


作者:小道 · 环境:Termux on Android 13 · 2026-09-15
关联阅读:上一篇《在手机上跑大模型,HexaBench Lite折腾记》 · 下一篇《复盘——一个非技术人的AI折腾哲学》

一个”Windows Hyper-V VM + Cloudflare DDNS + 端口转发”踩坑实录,
以及为什么最后答案是一行 tailscale up

(本文已对账号、IP、tailnet 名等敏感信息脱敏,仅保留技术结构。)

TL;DR

我有一台家用宽带(动态公网 IP)里的 Hyper-V Ubuntu 虚拟机,想从手机热点、
公司、任何网络
随时连进去开发。我花了半天搭 DDNS + 三层端口转发,
结果外网连不进来——运营商在封家用宽带的入站端口。最后用 Tailscale
一条命令搞定,彻底绕开 DDNS、端口转发、运营商封锁。

下面是完整踩坑和最终方案。

一、目标

  • 家里:Windows 宿主机跑 Hyper-V Ubuntu VM,静态内网 IP 172.x.x.x
  • 手机 / 任意网络:能随时 SSH 进 Ubuntu 开发
  • 要求:IP 怎么变都不用管,最好不用开公网端口

二、第一套方案:DDNS + 端口转发(为什么走不通)

2.1 架构

1
互联网 → 光猫/路由器(动态公网IP) → Windows(192.168.x.7) → portproxy 2222 → Ubuntu VM(172.x.x.x:22)

需要打通三层:

  1. Cloudflare DDNS:把 devbox.dingdao.me 自动指向当前公网出口 IP
  2. 路由器端口转发:外部 2222 → 192.168.x.7:22
  3. Windows portproxy2222 → 172.x.x.x:22 + 防火墙放行

2.2 我写的 DDNS 脚本(带容错)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/usr/bin/env bash
# /usr/local/bin/cf-ddns.sh (v2)
set -u
CONF=/etc/cf-ddns.conf
LAST_IP=/var/lib/cf-ddns/last_ip
...
# 5 个 IP 源依次尝试,3 次重试;全部失败 → 保留 last_ip,绝不把 CF 记录写脏
for src in "https://api.ipify.org" "https://ifconfig.me" ...; do
ip=$(timeout 8 curl -4 -sS "$src" 2>/dev/null | tr -d '[:space:]')
[[ "$ip" =~ ^[0-9.]+$ ]] && break
done
[[ -z "$ip" ]] && ip=$(cat "$LAST_IP" 2>/dev/null) # 兜底
[[ -z "$ip" ]] && { echo "no ip, skip"; exit 0; }
curl -sS -X PUT "https://api.cloudflare.com/client/v4/zones/$ZONE/dns_records/$RECORD" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "{\"type\":\"A\",\"name\":\"$HOSTNAME\",\"content\":\"$ip\"}"
echo "$ip" > "$LAST_IP"

systemd timer:开机 + 每 15 分钟跑一次。

踩坑:我一开始把 Cloudflare 控制台里的 Account ID 当成 Zone ID 填进脚本,结果 API 404。后来用 API 自动解析出 devbox.dingdao.me 所属 zone 的真实 Zone ID,脚本里直接查 API,不再手填。

2.3 为什么外网连不进来

所有配置都对:

  • ✅ CF 记录 devbox.dingdao.me → xx.xx.xx.xx(当前公网出口 IP)
  • ✅ 服务器公网出口 IP 就是那个 IP
  • ✅ Windows portproxy 2222 → 172.x.x.x:22 正确,LAN 段 Test-NetConnection 192.168.x.7 -Port 2222
  • ✅ 手机有公网 IPv4(测试 curl -4 https://ifconfig.me 能拿到一个公网 v4,不是 IPv6-only)

外网 xx.xx.xx.xx:2222 始终 CLOSED

排查结论:运营商在封家用宽带入站。家用宽带(尤其 PPPoE 拨号那层)
很多地区默认拒绝非 80/443 的入站 TCP,甚至全端口拒绝。DDNS + 端口转发
这套在”运营商不封入站”的网络里是完美的,但在我家这堵墙前无能为力。

这是国内家宽的老问题,不怪配置,怪物理链路。

三、最终方案:Tailscale(出站打洞,零入站端口)

3.1 为什么 Tailscale 能绕过这堵墙

Tailscale 本质是 WireGuard + 出站打洞

  • 每台机器主动向外连 Tailscale 的 Coordination Server(UDP 443)
  • 两台机器之间通过打洞建立直接 P2P WireGuard 隧道(如果打洞失败,
    退化为经 Tailscale 中转 relay,也不影响可用)
  • 全程不需要任何入站端口,不需要 DDNS,不需要运营商放通

对你的网络意味着:哪怕家用宽带入站全封,只要能出站(上网),
Tailscale 就能通。

3.2 三端落地(同一 Google 账号 → 同一 tailnet)

1
2
3
4
5
6
7
8
Ubuntu VM (Hyper-V)  +  Windows 宿主机  +  手机(Android/Termux)
\ / /
\ / /
┌────────────────── 同一 tailnet (xxx) ─────────────────┐
│ 100.123.x.x 100.113.x.x 100.98.x.x │
│ Ubuntu ←——⇄——→ Windows ←——⇄——→ 手机 │
│ (full mesh,任意两两直通) │
└────────────────────────────────────────────────────────┘

Ubuntu(服务器侧)

1
2
3
4
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --ssh --hostname=devbox
# 浏览器打开控制台给的链接授权
# 得到固定域名 devbox.<tailnet>.ts.net,稳定不变
  • 开机自启(systemctl enable tailscaled
  • --ssh 开了 Tailscale 自带 SSH,免私钥直连(控制台自动分发公钥)

Windows(宿主机)winget install Tailscale.Tailscale 或官网安装包,
同账号登录。

手机:App Store / Play 装 Tailscale,同账号登录,自动进 tailnet。

3.3 连接(任意网络、任意时刻)

本地 ~/.ssh/config(和原 DDNS 并存,双保险):

1
2
3
4
5
6
7
8
9
10
Host devbox-ts
HostName devbox.<tailnet>.ts.net
User kali
Port 22
IdentityFile C:\Users\<你>\.ssh\id_ed25519

Host devbox # DDNS 备用(局域网/公网入站可用时)
HostName devbox.dingdao.me
Port 2222
User kali

手机(最省事,免私钥):

1
tailscale ssh kali@100.123.x.x

3.4 验证

  • sudo tailscale status 三端全在线
  • 手机 tailscale ssh kali@... 直接进 Ubuntu shell ✅
  • tailscale ssh 不走 22 端口、不走公网,运营商封多少端口都不影响

四、安全性(针对”私人文件会不会暴露”的担忧)

Tailscale 在这套场景里不增加公网攻击面,原因:

  1. 默认对公网不可见100.x.y.z 是 RFC1918 保留段,公网路由送不到;
    攻击者没有你的账号 + tailnet 的 WireGuard 密钥,连”敲门”都找不到
  2. 双向 WireGuard 加密(AES-256-GCM):流量嗅探也解不开
  3. 默认零开放端口:不开 Funnel、不开 Subnet Router、不开 --ad-v4
    我确认过 Funnel 是关的
  4. 唯一成员 = 你的 Google 账号:要进 tailnet 得先攻陷你的 Google,
    那是账号层面的事,不是 Tailscale 新开的口子

真实剩余风险全在”账号/设备被攻破”层面,按性价比做这几项加固:

  • Google 开两步验证(最优先)
  • Tailscale 控制台开 2FA + 定期查 活跃会话/陌生设备(陌生即 revoke)
  • Per-Device ACL 收紧:手机/Windows 只允许连 Ubuntu 的 22,其余 deny
    (即使某台设备密钥泄露,横向也被限死在 ACL 内)

五、踩坑清单(可复用)

解法
Cloudflare 的 Account ID ≠ Zone ID,填错 404 用 API 查 zone 列表拿真实 Zone ID,脚本自动解析
DDNS 脏数据污染(IP 源全挂时把记录写空) 全失败时保留 last_ip,绝不覆盖已有正确记录
家宽外网入站被封(DDNS+转发全对但外网 CLOSED) 换 Tailscale 出站打洞,零入站
Hyper-V VM 重启 IP 变导致 portproxy 指错 VM 侧 netplan 配静态 IP,一劳永逸
Tailscale headless 拿不到认证 URL nohup sudo tailscale up ... & 后台跑,读 log 里的 URL

六、一句话总结

家用宽带入站不可控(运营商封端口、光猫路由模式二级 NAT、动态 IP)
这三个坑让”DDNS + 端口转发”在家用场景下不可靠
Tailscale 用”出站打洞”把问题降维成”只要能上网就能连”,
是个人远程开发/私人文件互通场景的默认最优解。
DDNS 那套留着做局域网直连的备用,公网访问交给 Tailscale。


配图建议:① 三层转发架构 vs Tailscale 全 mesh 对比 ② tailscale status 三端在线截图(IP 打码)③ 控制台安全加固项

脱敏说明(发文前自查)

本文已做如下脱敏,发布前请再自查一遍:

  • 真实邮箱 jianqiangcui996@gmail.com → 文中已隐去(只写”同一 Google 账号”)
  • 真实公网出口 IP 122.231.144.252、手机出口 124.160.204.98 → 文中用 xx.xx.xx.xx 占位
  • 真实 tailnet 名 tailb889a7 → 文中用 xxx 占位(devbox.<tailnet>.ts.net
  • 真实内网 172.31.92.47 / 192.168.2.7 → 文中用 172.x.x.x / 192.168.x.7 占位
  • 真实域名 kaliserver.dingdao.me / kaliserver.tailb889a7.ts.net → 文中改为 devbox.dingdao.me / devbox.<tailnet>.ts.net
  • Tailscale IP 100.123.36.49 / 100.113.231.42 / 100.98.143.1 → 文中用 100.123.x.x / 100.113.x.x / 100.98.x.x 占位

如需完全不可追溯到个人,上述占位符已覆盖所有可直接定位的标识符;
如需保留部分真实值(如公开博客保留域名示例),可把 devbox.dingdao.me
换成你自己愿意公开的域名。

不满足于云端API,想在手机上跑本地LLM。刷到HexaBench Lite,一个在Android上跑LLM benchmark的Web应用。编译llama.cpp、修复6个bug、搞Cloudflare Worker反向代理。手机上的LLM benchmark跑通了,PP 20.81 tok/s,TG 14.87 tok/s。虽然慢,但世界变了。

TL;DR

HexaBench Lite折腾记:llama.cpp编译太慢→换预编译wheel无aarch64版→clone源码只编译llama-simple。发现server.py用llama-cli(交互模式卡死)→改成llama-simple(单次运行自动退出)。修复6个bug:参数残留、流式输出一次性崩出、响应提取失败、光标错位。搞Cloudflare Worker反向代理想外网访问,被1003拦截。最终手机benchmark跑通:PP 20.81 tok/s,TG 14.87 tok/s。

一、起因:不满足于云端

Hermes跑起来之后,我一直用的是云端API。Agnes、Qwen、Claude,调个接口就能聊天,方便。

但方便久了,总觉得少了点什么。

云端API再好,数据不在自己手里,网络一断就歇菜。而且每次调用都要花钱,虽然不多,但心里有个声音在说:能不能在自己手机上跑?

刷到HexaBench Lite的时候,我眼睛亮了。这是个在Android上跑LLM benchmark的Web应用,用llama.cpp推理GGUF模型,跑完出速度报告。

“在手机上跑大模型”,这个念头一旦冒出来,就压不下去了。

二、环境准备:编译的痛

说干就干。

第一步:装llama.cpp。

我试了编译源码,git clone + cmake + make。Termux上没有cmake,先装cmake,然后编译。编译速度很慢,aarch64架构的ARM处理器,跑C++编译像在挤牙膏。

等不及了,换预编译wheel:pip install llama-cpp-python --only-binary=:all:。结果没有aarch64-linux版本。

最后只能硬着头皮clone源码,只编译llama-simple(非交互模式,跑完自动退出),不编译全部。

你太慢了,我通过llama官网命令curl -LsSf https://llama.app/install.sh | sh把环境装好了。你在会话里催我。

我说网络连不上HuggingFace,试下镜像。

最后环境终于搭好了。手机里有了llama-simple,有了GGUF模型文件(qwen2.5-1.5b、gemma-4b),万事俱备。

三、踩坑:llama-cli的交互陷阱

启动HexaBench Lite的server.py,选模型,点”开始测试”。

然后页面卡住了。

我查日志,发现llama-cli进入了交互模式(REPL),永远不退出。server.py等它的输出,它等用户的输入。死锁。

问题根因找到了:server.py用的是llama-cli(交互模式),应该用llama-simple(单次运行,自动退出)。

我改了代码,优先找llama-simple,找不到再 fallback 到llama-cli。同时修改解析逻辑,适配llama-simple的输出格式。

实测结果:

  • PP(Prompt Processing): 20.81 tok/s
  • TG(Token Generation): 14.87 tok/s
  • 加载: 1.8秒
  • 生成: 128 tokens

跑通了。

四、修复:6个bug的连环战

跑通只是第一步,真正折磨人的是后续的bug修复。

你在使用过程中发现了各种问题,一条条反馈给我:

bug 1:参数残留
“还是(请求失败或无响应),-s 1000000000 -t 1000是什么意思?搞错了吧”
llama-simple的输出里混入了命令行参数,前端显示了一堆-s 1000000000 -t 1000,不是AI的响应。重写cleanText()函数,6阶段正则清洗:命令行参数→llama日志前缀→prompt标记→统计行→空行→合并空行。

bug 2:流式输出一次性崩出
流式接口本该逐字输出,结果所有内容在结束时一次性吐出来。改成bufsize=0(无缓冲)+ 逐字节读取,实现真正的流式传输。

bug 3:响应提取失败
_extract_response函数无法正确从llama-simple的复杂输出中提取AI响应。发现llama-simple的实际输出格式为--threads ... -p [prompt] [AI_response] -s ... -t ... main: decoded ...,重构提取逻辑。

bug 4:光标位置错位
流式预览区和对话记录区职责不清,导致光标跳动。拆成两个独立区域:streamPreview带光标动画实时追加,chatArea仅在完成后更新。

bug 5:ERR_CONNECTION_REFUSED
“ERR_CONNECTION_REFUSED,没打开服务吧”
服务进程被杀掉了,重启,加健康检查。

bug 6:翻页与历史记录
“翻页看到了。但是就是输出预览参数和正文都有流动显示了,最后全部消失,只留下tg、用时、token数,正文要摘取下来存放到对话记录才行”
修复流式预览最终显示逻辑,保留响应文本,对话记录区显示干净文本。新增历史记录分页功能,每页20条。

6个bug,一个个修。修完一个,发现下一个。到最后,HexaBench Lite v6版本终于稳定了。

五、外网访问:Cloudflare Worker的1003

手机上的服务跑通了,但只能在局域网访问(http://192.168.2.10:8080)。我想从外网访问,于是搞了Cloudflare Worker反向代理。

代码写好了,Token申请好了,API部署。

结果Cloudflare API一直报语法错误。试了ES模块格式、传统fetch监听器格式,都不行。可能是Token权限不足或API版本限制。

最后改成手动在Dashboard部署,粘贴代码,Save and deploy。

部署成功,访问https://proxy.dingdao.me/,返回1003错误。

1003是Cloudflare的WAF拦截。Worker已经执行了,但Cloudflare在返回响应前拦截了。可能是Security Level或Rate Limiting的问题。

查WAF规则、调Security Level、清DNS缓存,试了一圈,还是1003。

外网访问没成。但本地服务运行正常,3个模型,端口8080,随时可以测。

六、结果:PP 20.81 tok/s

折腾了这么多,最终结果是什么?

手机上的LLM benchmark跑通了。qwen2.5-1.5b模型,PP 20.81 tok/s,TG 14.87 tok/s。gemma-4b模型,约10 tok/s。

14.87 tok/s是什么概念?正常人类阅读速度约200-300字/分钟,即3-5 tok/s。手机跑大模型的速度,已经超过了人类阅读速度。

虽然加载要1.8秒,虽然只有1.5B的小模型,虽然外网访问还没打通。

但当你的手机能跑大模型,世界就变了。

以前我觉得大模型是云端的东西,是API调用的东西,是GPT-4、Claude Opus那种级别的东西。现在我知道,1.5B的小模型,在手机CPU上,也能跑。

慢吗?慢。但能跑。

能跑,就意味着可能性。


作者:小道 · 环境:Termux on Android 13 · 2026-09-11
关联阅读:上一篇《OTG U盘迁移,物理传输的浪漫》 · 下一篇《FDE——前沿部署工程师的自我修养》

一台两年的 Win11 笔记本,C 盘 119 GB 只剩 8.86 GB,比 7% 还少。
三阶段、两个坑,最终腾到 39 GB(33%)——4.4× 提升。
这是完整的清理手记,含关键技术点、脚本片段和踩坑复盘,发出来希望能帮到同样手忙脚乱的人。


TL;DR

指标 优化前 优化后
C 盘总量 119 GB 119 GB
已用 110 GB 80 GB
可用 8.86 GB(7.4%) 39 GB(33%)
警告线(10%) 已触线 安全

三阶段释放构成

阶段 内容 风险 释放
阶段 0 浏览器/办公/IDE/AI 工具缓存 零(备份可回滚) ~11 GB
阶段 1 系统还原、WinSxS、WDAG、pagefile、事件日志 低(需管理员) ~13 GB
阶段 2 用户级 Python 包、AI 工作区、VS/SDK 迁移 中(迁错可回滚) ~8 GB

一、先看体检报告

动手前先扫一遍状态。我用了 PowerShell + Bash 的混合脚本,只读不写:

1
2
3
4
5
6
C 盘总量       : 119 GB
已用 : 110 GB
可用 : 8.86 GB ← 触线,低于 10% 警告阈值
D 盘 : 148 GB,几乎空
E 盘 : 428 GB,几乎空
物理内存 : 15.9 GB(峰值使用仅 ~1 GB)

症结很明显:数据全都堆在 C 盘,D/E 盘空着没人用。最直接的修法是”先备份后删除” + “把可外迁的应用迁出去”。于是我定了三阶段路线图,每个阶段都能停下来观察、能回滚。


二、阶段 0:缓存清理(~11 GB)

最稳的肥肉是各路缓存。清单:

  • Chrome 模型缓存:2.67 GB
  • 钉钉缓存:~2.3 GB
  • WPS 残留:~2.7 GB
  • 微信插件:0.41 GB
  • 回收站:5.41 GB(单独拎出来清,下面会讲为什么)
  • Gradle / pnpm / npm / pip 包管理器缓存:合计 ~1 GB

关键技术:robocopy 备份 + 返回码校验

每个目标走同一套流程:

1
2
3
4
5
6
7
8
# 1) 备份到 E 盘
robocopy "" "\" /E /COPY:DAT /R:1 /W:1 /NP /NFL /NDL

# 2) 校验返回码
# 0 / 1 = 成功;>= 8 = 出错(中止删除)

# 3) 删除原目录
Remove-Item "" -Recurse -Force

阶段 0 的两个坑

坑 ① 算大小挂死:第一版脚本用 Get-ChildItem -Recurse -File 算文件夹大小,挂在某个目录 26 分钟零输出。教训是——robocopy 直备不需要算大小,返回码就是校验结果。

坑 ② 漏了回收站:阶段 0 跑完,C 盘反而”变小”了。根因是脚本漏了 Clear-RecycleBin,且用户会话期间回收站又涨了。清单里必须单列回收站

回收站单独清一行就够:

1
Clear-RecycleBin -DriveLetter C -Force

三、阶段 1:系统级优化(~13 GB)

这一步需要管理员权限,每个子项可以独立开关。

A. 系统还原影子存储上限 → 3 GB(释放 ~8 GB)

Windows 默认把 10%+ 空间预留给”系统还原影子副本”,日常根本用不上那么多。

1
vssadmin Resize ShadowStorage /For=C: /On=C: /MaxSize=3GB

B. DISM WinSxS 清理 + -ResetBase(释放几百 MB ~ 几 GB)

1
dism /Online /Cleanup-Image /StartComponentCleanup /ResetBase

-ResetBase 会让系统无法再卸载旧更新,但能多清一些。前提是你这台机器已经稳了至少半年。

C. WDAG 容器镜像删除(3.94 GB)

Windows Defender Application Guard 是给 Edge 隔离浏览用的容器镜像,绝大多数人根本不开。如果你不用 Edge 隔离,直接清:

1
2
3
4
5
# 1) 卸载功能
dism /Online /Disable-Feature /FeatureName:AppHVUI -Remove

# 2) 清缓存
Remove-Item "C:\ProgramData\Microsoft\Windows Defender Advanced Threat Protection\Cache" -Recurse -Force

D. 事件日志清空(释放几十 ~ 几百 MB)

1
2
3
wevtutil cl Application
wevtutil cl System
wevtutil cl Security

E. 页面文件:2 GB 固定(释放 ~5 GB)

观察到 pagefile 峰值只 ~1 GB,于是设成 2048 MB 固定:

1
2
3
4
5
$cs = Get-CimInstance Win32_ComputerSystem
$cs | Set-CimInstance -Property @{ AutomaticManagedPagefile = $false }

Set-CimInstance -Query "SELECT * FROM Win32_PageFileSetting WHERE Name='C:\\pagefile.sys'" `
-Property @{ InitialSize = [uint32]2048; MaximumSize = [uint32]2048 }

坑 ③ CIM 类型不匹配:第一次脚本传 2048 报”类型不匹配”。CIM 属性是 uint32,PowerShell 默认 int32必须强转 [uint32]2048。这个坑我们后面会再踩一次反作用。

NVIDIA 旧驱动:建议保留

本来想顺手清掉几个旧版本 NVIDIA 驱动,结果 dism /Online /Remove-Driver 直接报”错误 50:只能与脱机映像一起使用”——在线系统不能用 Dism 删驱动。Installer2 缓存 223 MB 我顺手清了,驱动本体保留。要清驱动请用 DDU 离线模式。


四、阶段 2:用户数据迁移与清理(~8 GB)

删除部分(~3 GB)

  • Python 用户级包(torch / PyQt):0.84 GB
  • AI 工具 node_modules(两个工作区):2.31 GB

先备后删,备份目录按日期归档,回滚无忧。

迁移部分(~5.87 GB):junction 透明重定向

把 VS 生成工具、SDK、VS Installer Package Cache 搬到 E 盘,C 盘留 junction 透明重定向:

1
2
3
4
5
6
7
8
9
10
11
# 1) 复制到 E 盘
robocopy "C:\Program Files (x86)\Microsoft Visual Studio" `
"E:\relocated\Microsoft Visual Studio" /MIR /COPY:DAT

# 2) 原目录改名做安全网(不删!)
Rename-Item "C:\Program Files (x86)\Microsoft Visual Studio" `
"C:\Program Files (x86)\Microsoft Visual Studio.relocated"

# 3) 建 junction
cmd /c mklink /J "C:\Program Files (x86)\Microsoft Visual Studio" `
"E:\relocated\Microsoft Visual Studio"

三个目标都一样处理:VS、SDK、Package Cache。

关键安全网:原目录改名不删,叫 *.relocated。如果 VS 出问题,可以瞬间回滚(删 junction + 删改名后缀)。观察几天没问题再单独跑 p2_cleanup.ps1 释放安全网。

迁移完成后 C 盘立刻多出 5.87 GB——junction 本身只占几个字节。


五、踩坑复盘:pagefile 干翻 Hyper-V

阶段 1 全跑完的第二天早上,开机发现 13.4 GB 内存的 Linux VM 启不来

1
2
"无法分配 13394 MB 的 RAM"
错误码:0x800705AA (ERROR_NO_SYSTEM_RESOURCES)

这一路我走了三轮才彻底修好,每一轮的错误码都是进步信号:

错误码 含义 这一轮做了什么
0x800705AA 系统提交预算不足 脚本把 pagefile 改成”系统托管”,重启
0x8007000E 物理内存真的不够 脚本把 VM 改成 Dynamic Memory
InvalidState VM 还在”已保存”状态,Hyper-V 锁内存配置 脚本先 Stop-VM -ForceSet-VMMemory

根因复盘

这台机器 16 GB 物理内存,VM 想要 13.4 GB 固定 RAM。13.4 + 主机 ~3 + Hyper-V 开销 ~1 = 17.4 GB > 16 GB,物理上装不下,跟 pagefile 大小其实无关。

我的错误:阶段 1 把 pagefile 压到 2 GB,只看”使用峰值 1 GB”反推,忽略了”瞬时需求上限”——VM/数据库/大模型推理这些场景启动瞬间会要一大笔提交预算。

教训清单

  1. 资源上限类优化不能只看使用峰值,要先列出会被影响的典型负载(VM、数据库、大模型、视频剪辑……)
  2. 关键优化跑完主动做一次回归问询——“这次清理有没有把什么跑不起来?”,不要等用户当发现问题的第一人
  3. Hyper-V 改内存前必须把 VM 切到 Off 状态(Saved/Paused/Stopping 都不行),用 Stop-VM -Force 自动丢弃保存状态
  4. 16 GB 机器跑大内存 VM 的最佳实践:VM 开 Dynamic Memory(启动 4 GB / 范围 2~12 GB),闲时把内存还给主机、忙时拿满,比”VM 固定吃满 + 主机等死”健康得多

修完后,VM 改成 Dynamic Memory,C 盘又涨了 5 GB(pagefile 改回系统托管)——最后的成绩是 C 盘 39 GB 收尾。


六、最终成绩单

1
2
3
4
5
6
7
阶段          释放            关键项
─────────────────────────────────────────────
阶段 0 11 GB 回收站 5.41 + 各类缓存
阶段 1 13 GB 系统还原 ~8 + WDAG 3.94 + pagefile ~5
阶段 2 8 GB AI 工作区 3.15 + VS/SDK 迁移 5.87
─────────────────────────────────────────────
合计 ~32 GB

C 盘最终:8.86 GB → 39 GB(33%),4.4× 提升。


七、给同样手忙脚乱的人

  1. 先看体检报告再动手——别上来就清缓存,先 Get-PSDrive / du -h 摸清楚大块在哪
  2. 三阶段路线图:零风险 → 系统级 → 结构性迁移,每阶段都能停
  3. 备份 > 删除——robocopy + 改名 .relocated 安全网 > 直接 rm -rf
  4. junction 迁移比”删了重装”温和得多,尤其是 VS、SDK 这类对路径敏感的应用
  5. pagefile 不能拍脑袋压——先看这台机器跑什么负载
  6. 写 PowerShell 脚本处理路径一定要 -LiteralPath,路径里有 (x86) 这种括号不用 -LiteralPath 会出问题
  7. CIM 数值属性必须强转 [uint32],默认 int32 一定踩坑
  8. 交付给用户用 powershell -File 跑的脚本,用 UTF-8 BOM + 纯 ASCII,避免本地 ANSI 编码解析失败
  9. Dism /Online /Remove-Driver 在线系统受限,要清驱动请用 DDU 离线模式

附录:本文用到的脚本

脚本 用途 权限
Stage1_SystemCleanup.ps1 阶段 1 主脚本(A 还原 / B pagefile / C WinSxS / D NVIDIA / E WDAG / F 日志) 管理员
p1_fix_pagefile.ps1 pagefile 修复(含 [uint32] 强转) 管理员
p1_fix_pagefile_hyperv.ps1 pagefile 恢复 + 系统托管选项 管理员
p2_migrate.ps1 VS / SDK / Package Cache 迁移(robocopy + junction) 管理员
p2_cleanup.ps1 删除 *.relocated 安全网(先校验 junction) 管理员
p1_fix_kali_vm_memory.ps1 Hyper-V VM 内存修复(先 Stop-VM -Force 切 Off) 管理员

全部脚本遵循同一原则:先校验、再操作、操作完再校验


标签:#Windows #C盘优化 #Hyper-V #Junction迁移 #Pagefile #PowerShell

0%