小道的技术笔记

记录折腾、认知与闭环

从一块 17.9GB 的镜像文件开始,到一台能自动接无线网卡、能被自然语言驱动、图形界面卡死还能远程自救的渗透实验环境。
全程真实踩坑,所有敏感信息已脱敏,仅涉及自有设备与授权测试。


这个项目要解决什么问题

三个朴素的需求:

  1. 在 Windows 工作机上跑一台 Kali 虚拟机
  2. 虚拟机要能用上真实的 USB 无线网卡(做无线安全测试)
  3. 我 Linux 命令不熟,想让 AI 把中文需求翻译成命令,边用边学

听起来都不难。实际做完发现,这三条路上的坑,一个比一个深。

第一阶段:让 Kali 在 Hyper-V 里活起来

WSL2 装过,不等于有 Hyper-V

这是第一个认知刷新:WSL2 只需要”虚拟机平台”组件,它不等于完整的 Hyper-V。完整的角色要自己开:

1
dism /Online /Enable-Feature /FeatureName:Microsoft-Hyper-V /All /NoRestart

必须重启,vmms 服务和 PowerShell 模块才会出现。

这里踩了个很阴的坑:用提权方式执行脚本时,命令里嵌套引号加输出重定向会被吞掉——脚本静默失败,日志是空的,你以为成功了其实什么都没发生

教训:提权跑复杂脚本,一律写成独立 .ps1 文件用 -File 调用,日志在脚本内部落盘。

官方脚本建机,但权限是个坑

Kali 官方镜像自带建机脚本,一条命令完事。但打开控制台就报”权限不足”——因为虚拟机是在管理员会话里创建的。

解法是把自己加进 Hyper-V Administrators 组,然后必须注销重登录

汉化:一个 Ubuntu 惯害的坑

第一反应装 language-pack-zh-hans——查无此包。这是 Ubuntu 专属包,Kali 基于 Debian,根本没有。

正确姿势:

1
2
3
4
sudo apt install -y fonts-noto-cjk locales
sudo sed -i 's/^# *zh_CN.UTF-8/zh_CN.UTF-8/' /etc/locale.gen
sudo locale-gen
sudo update-locale LANG=zh_CN.UTF-8

中间还遇到 locale-gen zh_CN.UTF-8 参数被静默忽略、只生成英文 locale 的怪事——最后靠手动改配置文件解决。

2.5GB 大升级翻车,apt 半残

Kali rolling 一周不更就是 1100+ 个包。升级到末尾两个包下载失败(镜像站 IPv6 解析问题),而 apt 的行为是:下载阶段有任何失败,整个配置阶段就不执行

于是系统停在”一半解包、一半没配置”的状态,之后装任何新包都报依赖错误,util-linux 卡死在两个版本之间。

修复链必须按顺序:

1
2
3
sudo dpkg --configure -a
sudo apt --fix-broken install -y
sudo apt full-upgrade -y

顺便回答一个很多人问的问题:加内存加 CPU 能不能快点?不能。 apt 是单线程串行的,瓶颈在 dpkg 逐个解包配置,跟硬件无关。

代理:让虚拟机的流量出海

思路:Default Switch 是 NAT,虚拟机的默认网关就是宿主机,所以代理地址就是网关地址。

三个要点:v2rayN 要开”允许局域网连接”;新版只有一个混合端口(socks+http 同端口);防火墙放行要写整个私网段——因为 Default Switch 每次重启随机换网段

最后写了个 NetworkManager 钩子:每次网络变化自动探测网关、检测代理可达才写配置,v2rayN 没开就自动清掉。一次配置,IP 怎么变都不用管。

第二阶段:让 AI 驱动这台机器

Gemini CLI:授权成功,但被关门了

先试 Gemini CLI,OAuth 授权一路成功,然后服务端拒绝:

This client is no longer supported for Gemini Code Assist for individuals.

查了才知道,个人免费通道已经关闭,官方让迁移去新产品。授权成功也没用,门在服务端焊死了。

qwen-code:好用,但中文打不进去

换成阿里的 qwen-code,免费额度够用,但卡在一个诡异的问题上:终端里死活打不了中文,图形程序里一切正常。

根因:这类 CLI 工具用 raw 模式逐键读键盘,直接打断了输入法的组合输入流程。换终端模拟器也没用。

最后换了个思路——别在终端较劲,用图形界面

  • VS Code + Cline 插件:图形程序,中文输入天然正常;agent 模式能读意图、调内置终端执行命令、读输出继续下一步

真正高收益的操作是写了一份 .clinerules

1
2
3
4
5
6
7
8
# 环境
- Kali,Hyper-V 虚拟机,我是 Linux 命令初学者,全程中文

# 工作方式
1. 我用自然语言描述目标,你翻译成命令
2. 执行任何命令前,先用一行中文解释它做什么
3. 危险操作先列风险等我确认
4. 每次任务结束,把用到的命令按主题追加到笔记文件

这份文件每次对话自动加载。第 2 条保安全,第 4 条让 AI 自动帮你攒一本私人命令手册——用两周,那本笔记比任何教程都贴合你的实际使用。

第三阶段:无线网卡,全程最深的水

先说残酷事实

Hyper-V 不支持 USB 直通,也不支持把内置网卡共享给虚拟机。 虚拟机里天生没有任何无线接口。

唯一出路:微软官方的 usbipd-win,把 USB 设备通过网络”递”进虚拟机。

五个坑,按踩的顺序

坑 1:Windows 断网了。 第一次接入成功后宿主机直接断网——因为那块网卡就是 Windows 上网用的。接入是独占的。后来插了第二块网卡才分工明确:AIC8800D80 给 Windows 上网,RTL8188EU 给 Kali 测试。

坑 2:芯片选错了。 AIC8800D80 是国产小众芯片,Linux 内核没驱动。而 RTL8188EU 是渗透圈经典入门卡——内核自带驱动(注意驱动名是 rtl8xxxu 不是 r8188eu),监听和注入都有支持。

坑 3:固件启动失败。 设备进来了,驱动认出来了,固件加载报 -11。固件文件明明存在。最后发现解法朴素得离谱:物理拔插一次,重新接入,就好了

坑 4:注入测试误判。 广播探测测试显示”无注入”,差点否掉这张卡。实际上广播探测本来就不该有回应,必须带目标定向测试

1
2
aireplay-ng -9 -e "SSID" -b BSSID wlan0
→ Injection is working! 30/30: 100%

坑 5:用完之后托盘里没有 WiFi 了。 接口残留监听模式(ip link 里能看到 radiotap 字样),而 NetworkManager 会主动忽略监听模式的接口。新版 aircrack-ng 也不再重命名接口,stop 之后状态经常残留。

战果:完整的 WPA2 审计链路

1
2
3
4
airmon-ng start wlan0                 # 进监听模式
aireplay-ng -0 5 -a BSSID wlan0 # 踢客户端下线,逼它重连握手
airodump-ng -w cap -c 8 --bssid ... # 抓包,看到 WPA handshake 即成功
aircrack-ng -w 字典 cap-01.cap # 破解

结果:测试了 2 个字典词、0.1 秒,密码出来了。

因为我家路由器密码是 8 位纯数字日期格式。也就是说,任何人在楼下,几秒钟就能进我的网络。

这条链路对我最大的价值不是攻击,是把自己家的洞补上了。 当天就改成了强密码。

第四阶段:把一切自动化(包括救它自己)

日常自动化

开机自动接无线网卡,做成了 systemd 服务,带三重保险:

  • 等默认路由出现(最多 60 秒)
  • 遍历所有默认网关逐个尝试(虚拟机多网卡时只取第一条会取错)
  • 失败自动重启服务重试

再配两条别名:wifimon 进监听模式,wifinorm 恢复正常。

顺手把内存从”动态内存”改成固定 4GB——动态内存在 Linux 客户机里靠 balloon 驱动伸缩,宿主有压力时会造成 I/O 停顿,怀疑正是图形界面卡死的诱因之一。

救援体系:图形界面卡死怎么办

这个环境用下来,Xfce 图形界面卡死过好几次(内核活着、桌面死了)。救援的阶梯是:

第 1 级:SSH 远程重启图形界面(最理想,只丢桌面会话)

1
ssh kali@ "sudo systemctl restart lightdm"

配套:免密 SSH + 定向的 sudo 免密规则(只放行 lightdm/NetworkManager 等几个重启命令,不是全局放开)。

第 2 级:优雅关机再开机。

第 3 级:硬断电。 这里有个反直觉的知识点:Stop-VM -Force 不是硬关机,-Force 只是跳过确认,关机方式仍是优雅的。真正的断电是 -TurnOff

而卡死的系统连优雅关机都完成不了,VM 会 wedge 在 Stopping 状态,所有电源操作报错。解法是重启 vmms 服务解开死锁,再硬断电。

最后把整个阶梯做成了一键脚本:查心跳 → 没运行就开机 → 心跳异常直接跳断电 → SSH 重启图形界面 → 优雅关机 → 解死锁硬断电 → 还不行就打印 VM 的 GUID 让你手动杀工作进程(vmwp.exe 的命令行里带 GUID,能精确定位,不会误杀 WSL2)。

实测有效。一次真实的卡死,脚本走完全程,机器自己回来了。

脚本本身也是坑里爬出来的

  • 写 PowerShell 脚本带中文注释,必须存 UTF-8 with BOM——Windows PowerShell 5.1 默认按 ANSI 读,中文被错误解码后直接语法报错
  • 图形界面卡死时,Hyper-V 的 KVP 组件恰好也不会上报虚拟机 IP——最需要救援的时刻拿不到 IP。解法是平时把最后已知 IP 缓存下来,卡死时用缓存连

复盘:最值得记住的七条

  1. WSL2 ≠ Hyper-V,完整角色要单独启用并重启
  2. apt 下载失败会留半安装状态,锁死依赖树;修复按 dpkg --configure -a → -f install → full-upgrade 顺序
  3. 虚拟机抢 USB 网卡前,先确认宿主机有另一条上网路径
  4. 工具报错不一定是配置错,可能只是测试方法不对(注入测试必须定向)
  5. 别在终端 TUI 上跟输入法较劲,图形界面 + agent 是更聪明的选择
  6. 凡是每次都要重跑的操作,做成 systemd 服务或别名,一次配置永久收益
  7. PowerShell 脚本带中文,存 UTF-8 with BOMStop-VM -Force 不是硬关机,-TurnOff 才是

这台机器现在的样子

  • 中文系统 + 拼音输入法
  • 开机自动接无线网卡,托盘直接连 WiFi
  • 代理自动跟随宿主机,IP 变了不用管
  • VS Code + AI agent 自然语言驱动,自动攒命令笔记
  • 无线监听 + 注入 100% 可用
  • 图形界面卡死时,Windows 侧双击一个脚本完成救援

从裸镜像到这套东西,断断续续两天。中间十几个坑全部定位到根因,并且每一个都沉淀成了自动化、脚本或者笔记。


最后强调边界:本文所有操作都在自己的虚拟机、自己的路由器上完成。
对他人网络抓包、破解,在国内属于明确的刑事风险,与技术水平无关。
想练手,TryHackMe、Hack The Box、VulnHub 有的是合法靶场。

发现Termux占用了22GB。一台手机,被一个AI工具吃掉了22G。逐层排查后发现,/usr/tmp里躺着9GB的编译残留,state.db里存着5万条消息。清理只需一顿饭的功夫,但面对微信图片和录屏,我犹豫了。

TL;DR

Termux占用22GB,核心数据分布:/usr/tmp 9GB(pip/rust编译残留),state.db 627MB(5万条消息),downloads 954MB(重复备份)。第一波清理释放10GB。面临选择:微信图片2.5G、录屏11G,这些是手机数据,不是Termux的,决定不动。长期运行的代价,是永远在清理自己制造的垃圾。

一、起因:22GB的震惊

有一天,我无意间看了一眼手机的存储空间。

已用193GB,剩余33GB。总容量226GB。

我以为是哪个APP吃掉了空间,结果一查,Termux占了22GB。

22GB。一个终端模拟器,加上我跑在里面的Hermes Agent,吃掉了22GB。

我当时的第一反应是:不可能。我装的东西不多,Hermes本身也就几百MB。

然后我开始逐层排查。

二、排查:垃圾是怎么堆起来的

我用了du -sh命令,一层层看下去。

第一层:/usr/tmp,9GB

这个目录是Termux的系统临时目录。里面全是pip和rust编译时留下的临时文件。

我想起之前装cryptography、llama.cpp的时候,编译过程会产生大量中间文件。编译完了,这些文件没被清理,一直留在那里。

9GB。全是编译残留。

第二层:/home/.hermes/,2.6GB

这是Hermes的主数据目录。再往里看:

  • state.db:627MB。这是Hermes的会话数据库,里面存了5万条消息。
  • hermes-agent/:1.3GB。这是Hermes的源码和Python venv。

627MB的state.db让我愣了一下。5万条消息,平均每条12KB。有些消息可能包含了完整的代码块、长文本、甚至图片的base64编码。

第三层:downloads,954MB

下载文件夹里有一个”近期合集.zip”,929MB。还有一个hermes_agent_src.tar.gz,651MB。

我想起之前做Hermes迁移的时候,打包了388MB的备份,后来又打包了651MB的源码包。这些包传完了,就没管了。

第四层:其他缓存

  • .cargo/:1.1GB(Rust包缓存)
  • .local/share/pnpm/:829MB(Node包缓存)
  • .cache/:612MB(pip缓存)

加起来又是2.5GB。

三、清理:动手还是不动手?

排查完了,面临选择。

第一波:零风险清理

/usr/tmp 9GB、pip缓存612MB、pnpm缓存829MB。这些是纯垃圾,编译完就没用的东西。

我执行了清理命令:

1
2
3
rm -rf /data/data/com.termux/files/usr/tmp/*
rm -rf ~/.cache/pip
rm -rf ~/.local/share/pnpm/store

释放了约10GB。

第二波:需谨慎确认

downloads里的重复备份文件(hermes_agent_src.tar.gz 651MB、近期合集.zip 929MB)。这些包已经用过了,不需要保留。

我删掉了它们。又释放了约1.5GB。

第三波:犹豫了

再往下查,发现了微信图片2.5GB、屏幕录屏11GB(49个文件,最早2020年,最近2026年)。

这些是手机数据,不是Termux的。但它们是Hermes运行过程中产生的吗?不是。它们是手机系统本身产生的。

但问题是:录屏11GB,最早的文件是2020年的。6年前的录屏,我还会看吗?

我犹豫了。

在阿里做销售的时候,我有个习惯:客户资料定期清理,过期的合同销毁,不再跟进的客户归档。但那是工作,有明确的SOP。

现在是自己的手机,里面的数据没有SOP。录屏里有工作的记录、孩子的视频、生活的片段。删掉6年前的录屏,万一以后需要呢?

最后我决定:只清理自己能确定的垃圾。微信图片和录屏,不动。

四、结果:从22GB到12GB

清理完成后,Termux的占用从22GB降到了12GB。

释放了10GB。

我建立了一个清理模式(scripts/workspace/cache三级目录结构),把临时文件、工作目录、缓存分开存放,方便以后定期清理。

但看着state.db里的5万条消息,我还是有点感慨。

5万条消息。有多少是真正有价值的?有多少只是”试了一下”、”问了一句”、”没跑通”的残骸?

五、教训:数据是资产也是负债

这件事给我上了一课。

数据是资产。5万条消息里,可能有我需要的答案、有价值的对话、重要的决策记录。

数据也是负债。627MB的数据库,查询会变慢,备份会变重,迁移会更麻烦。

长期运行的代价,是永远在清理自己制造的垃圾。

我以前觉得”留着总有用”,现在明白了”留着总占地方”。

不是所有数据都值得保留。有些东西,过期了就让它过期。


作者:小道 · 环境:Termux on Android 13 · 2026-09-04
关联阅读:上一篇《升级只需5分钟,修环境要5小时》 · 下一篇《OTG U盘迁移,物理传输的浪漫》

想把Hermes从一台手机迁移到另一台,388MB的数据加650MB的源码。网络传输(nc)断了,只收到30MB。最后我拔了U盘插到手机上,复制、校验、拔下来插到另一台手机。土办法,但稳。

TL;DR

Hermes跨设备迁移(192.168.2.23 → 192.168.2.22),备份388MB + 源码650MB。网络传输(netcat)中断,只收到30MB。改用OTG U盘物理传输,MD5校验一致。目标设备设置PATH,重启gateway,一切恢复。土办法 vs 标准方案:有时候最笨的方法最靠谱,但靠谱不等于可扩展。

一、起因:换个地方跑

Hermes在原来的设备(192.168.2.23)上跑得挺稳,但我想把它迁移到一台新设备(192.168.2.22)上。

原因很简单:原来的设备还有其他用途,Hermes需要一台更稳定的机器常驻。

打包数据很快。核心配置、技能、插件、会话记录,打包成hermes_complete_backup.tar.gz,388MB。源码hermes_agent_src.tar.gz,650MB。

接下来是传输。

二、翻车:网络传输断了

我首选的方案是局域网传输。两台手机都在同一个WiFi下,用nc(netcat)管道传输,速度快,不用拔插U盘。

源设备监听端口,目标设备连接接收。命令很简单:

1
2
3
4
5
# 源设备
nc -l -p 1235 < ~/hermes_agent_src.tar.gz

# 目标设备
nc 192.168.2.23 1235 > ~/hermes_agent_src.tar.gz

看起来完美。650MB的文件,按WiFi速度几分钟就能传完。

然后我等着。

等了很久,目标设备上的文件大小停在了30MB,不动了。

传输中断了。

原因可能是WiFi信号波动、Termux后台被系统杀掉、或者网络协议栈出了问题。总之,传了30MB就断了。

我重试了一次,还是断。

三、土办法:OTG U盘

我盯着那个停在30MB的文件,叹了口气。

然后我想起了我包里有个OTG U盘。

“还是失败,这样吧,我有个otg设备可以用,已经插到本机了。”我在会话里跟助手说。

接下来的操作,回到了20年前的计算机时代:

  1. 在源设备上找到OTG的挂载点(/mnt/media_rw/F69C43E49C439E4D)。
  2. 把650MB的源码包复制到U盘里。
  3. 拔下U盘。
  4. 走到目标设备旁边(其实就在旁边桌子上)。
  5. 把U盘插到目标设备上。
  6. 找到U盘里的文件,复制到目标设备的home目录。
  7. 校验MD5。

没有网络波动,没有后台杀进程,没有协议栈问题。物理连接,稳如老狗。

四、结果:MD5校验一致

复制完成,MD5校验:

1
2
源文件 MD5: ccae57dbacec900b2b88c2c8cb5c7a52
U盘文件 MD5: ccae57dbacec900b2b88c2c8cb5c7a52

一致。

解压,设置PATH环境变量,重启gateway。

hermes gateway restart

一切恢复。技能、配置、会话记录,全在。

五、感悟:靠谱 vs 可扩展

这次迁移让我对”土办法”有了新的认识。

在网络传输不稳定的情况下,OTG U盘物理传输虽然笨,但它是100%可靠的。只要U盘不坏,文件就不会丢。

但这也暴露了我这套架构的一个问题:全自建,无生产级兜底。

如果这是一台生产服务器,我应该有rsync增量同步、有断点续传、有自动化部署脚本。而不是靠手动打包、手动传U盘、手动设PATH。

土办法靠谱,但不代表它可扩展。

如果有一天Hermes要迁移到第三台、第四台设备,或者需要定期备份到云端,OTG U盘就不够用了。

但至少在那一刻,在这个226GB存储的Android手机上,这个土办法救了我的命。


作者:小道 · 环境:Termux on Android 13 · 2026-09-02
关联阅读:上一篇《22GB到12GB,一次手机空间大清理》 · 下一篇《在手机上跑大模型,HexaBench Lite折腾记》

作者按:本文所有测试均在本人自有设备、自有局域网内完成,仅用于安全研究。文中内网地址、MAC、真实口令均已打码,请勿对他人设备做同类操作。


0. 起因:闲着也是闲着,扫一下家里的摄像头

家里局域网里蹲着一台 360 智能摄像机,平时只用 App 看画面。某天突然想:它到底在网上”露”了几个口子?

于是有了这次从零开始的黑盒评估。

有意思的是,我的扫描环境没装 nmap。没关系,Python 标准库(socket)就够写一套能用的扫描器——后面会讲怎么做。


1. 资产测绘:先搞清楚对面是谁

目标是一台接入 Wi-Fi 的摄像头,内网地址记为 192.168.x.19(已打码)。

三步走:

  • 连通性ping 通,TTL=64,典型嵌入式 Linux 特征。
  • MAC 归属:查 OUI 库,厂商指向 成都全景智能科技——这正是 360 摄像头常见的代工厂。
  • 协议指纹:RTSP 的 WWW-Authenticate 头里直接写了 realm="360Camera",身份坐实。

小技巧:光看 IP 没用,MAC 的 OUI 前三位 + 协议 banner 是最快的设备指纹组合。


2. 端口扫描:干净得有点反常

全端口 TCP 扫描(1–65535)结果:

结果
开放 TCP 端口 仅 554(RTSP)
服务标识 ireader/media-server
Web 管理界面
SSH / Telnet
ONVIF / UPnP / SSDP / mDNS 全部静默

65535 个端口只开了 1 个,没有远程管理后台,也没有命令行入口。再丢 8 类畸形输入进去(超长字段、非法方法、截断报文等),服务端全部正确处理,无崩溃

客观说,这块比同价位一堆消费级摄像头要干净——很多廉价摄像头会顺手多开 80、23、8080 一堆口子。

攻击面小,是好事。但”唯一入口”往往意味着:它必须足够硬,否则一破全破。


3. 唯一的入口 RTSP:问题出在这里

554 端口跑的是 RTSP(实时流协议)。我做了三件事:

3.1 发现了一个”认证 oracle”

这是我这次最意外的发现。

按规范,RTSP 的摘要认证应该是”认证失败”和”认证成功但请求有误”不可区分的。但这台设备给出了截然不同的状态码:

  • 密码错误 → 稳定返回 401 Unauthorized
  • 密码正确 → 进入业务处理,返回 400 Bad Request

我用对照组验证过:admin / 错误密码 五次全 401,admin / 正确弱口令 十次全 400,复现率 100%。

这意味着什么? 攻击者不需要真的拉流,发一个请求看返回码,就能判定密码对不对。没有锁定、没有限速、没有告警——一个常见弱口令字典,几小时就能穷举完。

3.2 出厂默认口令已失效,但用户口令很弱

测试发现 admin / admin 这种经典出厂组合已经不通了(厂商至少改过一版默认值)。但设备开启 RTSP 取流后设置的那个口令,落在常见字典 Top 100 里——属于典型的”人设的弱密码”。

这条要讲清楚:这锅不完全是厂商的。很多用户开 RTSP 时随手设个好记的密码,等于把门虚掩。

3.3 两个空实现,不算漏洞

未认证状态下 GET_PARAMETER / SET_PARAMETER 返回 200,但 Content-Length: 0,判定是 no-op 空实现,不构成可利用漏洞。如实记录,不夸大。


4. 决定性一击:真的把实时画面拉下来了

拿到正确流地址(云台一路 /live/101、枪机一路 /live/201)后,走完整 RTSP 握手:

1
2
3
DESCRIBE → 200 OK(拿到完整 SDP)
SETUP → 200 OK
PLAY → 200 OK

两路都成功取到了 16KB 实时 RTP 数据

  • 视频编码:H.265
  • 音频编码:PCMA(G.711)
  • 会话已建立

翻译成人话:任何在同一局域网、知道那个弱口令的人,打开 VLC 输一行地址,就能实时看这两路画面。

1
2
vlc rtsp://admin:********@192.168.x.19:554/live/101
vlc rtsp://admin:********@192.168.x.19:554/live/201

(上面口令是打码占位,真实口令切勿外传。)


5. 风险定级

场景 等级 说明
仅局域网内 同网段任意设备可偷看实时画面
路由器做了 554 端口转发 / UPnP 映射公网 严重 全球任何人可访问

关键不确定性在路由器那一侧:我无法从内网确认它有没有被映射出去。这条决定了风险是”高”还是”严重”,得你自己去查。

顺带一提,组件 ireader/media-server 有 3 个已知的释放后使用(UAF)型 DoS 漏洞(CVE-2022-40016、CVE-2024-24262、CVE-2024-24260,均 CVSS 7.5)。版本号抓不到,我没做实际利用(那会打挂设备),但建议把固件升到最新


6. 给你的防护清单(干货)

按优先级:

  1. 立刻改 RTSP 密码
    360 App → 该摄像头设置 → RTSP 取流,换成 16 位以上随机串。别用生日、电话、admin 系。

  2. 查路由器公网映射

    • 端口转发 / 虚拟服务器里有没有 554 指向这台摄像头
    • UPnP 是否开启 → 建议直接关掉并清空映射表
  3. 不用 RTSP 就关掉
    不接 NVR / NAS / Home Assistant 的话,直接关 RTSP 视频流开关。关掉后它在网络上几乎零端口,日常用 App 看完全不受影响。

  4. 升固件
    走 App 官方渠道升到最新,堵上组件层已知漏洞。


7. 结语

这次评估最有意思的地方在于反差:

攻击面极小、协议栈稳健,却被一个弱口令 + 一个认证状态泄漏打穿。

设备本身做得不差,真正的风险来自”唯一入口缺乏防爆破机制” + “用户随手设的弱密码”。安全不是某个端口开没开,而是最弱那根链条扛不扛得住。

把上面 4 条清单过一遍,这台摄像头就能从”高/严重”回到”安心”。


免责声明:本文所有操作均在作者自有设备与授权网络内完成,用于安全科普。未经授权对他人设备、网络进行扫描或访问属于违法行为,后果自负。文中已对一切可定位到具体设备的敏感信息做脱敏处理。

— 完 —

废旧电脑跑本地大模型:从”装不上”到”能聊天”的完整踩坑实录

本地 LLM 部署 · 推理引擎选型 · 模型评测 · 一篇写给同好的实操笔记

前阵子清理硬盘,翻出几个 .gguf 格式的本地大模型文件。本来只是想”试试能不能跑”,结果一路踩坑——网络被墙、引擎不认新架构、模型命名是假的……折腾了一整圈,最终不仅跑通了,还顺手搭了一套可复用的评测环境。本文把整个过程原样记录下来,给想在二手/老旧机器上玩本地模型的你少走点弯路。

说明(脱敏):文中涉及的具体用户名、绝对路径、内网环境均已做泛化处理;代码里的路径请替换成你自己的实际目录。评测在纯 CPU 环境下进行,无独立显卡。

一、先摸底:老机器能不能扛?

本地推理最怕两件事:显存不够内存不够。我的这台机器配置很普通:

  • CPU:一颗 4 核的 Intel 老处理器(支持 AVX2,没有 AVX-512)
  • 内存:16 GB(空闲约 7~8 GB)
  • 显卡:无独显,纯 CPU 推理
  • 系统:Windows 10

结论是够用。常见的 1B3B 量化模型(Q4 级别)单文件 0.62 GB,全部能塞进内存。真正卡我的不是硬件,是下面这两关。

二、第一道坎:网络

做本地部署,第一步几乎都要从 GitHub / HuggingFace 拉工具或模型。但我的网络环境一度访问不了 GitHub(Releases、各类 ghproxy 镜像全都超时)。这直接卡死了最省事的方案:

  • llama-cpp-python 的预编译 wheel 在 GitHub Releases 上 → 拉不下来;
  • HuggingFace 本体 → 连不上;
  • 源码编译需要本地 C++ 工具链 → 这台机器没有。

后来网络恢复后,GitHub 能通了,但 Release 资产下载依然不稳(动不动中断)。最终的解法:用脚本对 Release 的 wheel 地址做分块断点续传拉到本地,再 pip install --no-index 离线安装;而 numpydiskcache 这类依赖走国内 PyPI 镜像(如腾讯云镜像)就很快。

经验:在国内环境装 Python 包,永远先加 -i https://mirrors.cloud.tencent.com/pypi/simple/(或清华/阿里云镜像),能省大量时间。

三、引擎选型:踩了一圈坑

GGUF 只是个文件格式,真正”让它说话”的是推理引擎。我先后试了三个:

引擎结果原因
ctransformers
从 PyPI 直装失败版本太旧,连 Llama-3.2 架构都不认识,遇到非标准架构直接崩溃。
gpt4all
走国内镜像装部分可用能跑标准 Transformer(Llama 系),但遇到非标准架构标签直接报错。
llama-cpp-python
wheel 断点续传 + 离线装成功底层是较新的 llama.cpp,架构兼容最好,三个模型全部加载成功。

结论先行:本地 GGUF 推理,无脑选 llama-cpp-python(或它的上游 llama.cpp)。它架构支持最全、社区最活跃,后面所有评测都基于它(0.3.35,CPU 版)。

四、三个模型,三种”人格”

手头(以及后来陆续往里加的)模型里,最初这三个最有代表性:

① Hermes-3-Llama-3.2-3B —— 最靠谱的主力

真实存在的开源指令模型(NousResearch 出品,Llama-3.2 底座)。中文连贯、会写代码、能出标准 JSON,安全护栏正常(明确拒绝危险请求)。规模虽只有 3B,但综合表现最好,推荐作为日常主力

② TinyLlama-1.1B-Chat —— 快但”莽”

1.1B 小模型,速度最快(CPU 下 ~22 token/s),但能力弱:中文指令遵循差、数学容易胡说、JSON 输出不稳。最要命的是——它没有安全护栏,在安全测试中直接把危险步骤列了出来。结论:适合做轻量草稿/分类,别用在面向用户或涉安全的场景

③ 那个”缝合怪”:qwen3.5-2B-deepseek-v4

这个命名本身就是警示信号——“Qwen3.5””DeepSeek v4”都不是真实发布的型号。扒开它的 GGUF 元数据和张量结构后发现:

  • 架构标签是自定义的 qwen35,但张量里混着 ssm_a / ssm_dt / ssm_conv1dMamba 系(状态空间模型)张量 + Transformer 注意力 → 这是一个来源不明的混合架构
  • 行为是推理型模型:先 <think> 思考再作答;
  • 意外的是,它的安全护栏是正常的(会引用法律拒绝危险请求)。

用对的引擎(llama-cpp-python)它能正常加载、能聊天(~11.5 token/s)。但来源和真实架构核实不了,我的态度是:当实验对象可以,当主力或上生产不行。

踩坑提醒:下载模型只认 HuggingFace 官方或可信作者。凡是出现”Qwen3.5 / DeepSeek v4 / 某某 Pro Max”这类不存在的型号名,基本都是引流或缝合模型,谨慎对待。

五、评测结果速览

我写了一套评测脚本(7 个维度:中文解释、数学推理、代码、知识问答、JSON 格式、安全护栏、指令遵循)+ 速度基准,全在 CPU 下实测。核心数据:

模型速度
(CPU token/s)中文能力代码/JSON安全护栏综合建议
Hermes-3-Llama-3.2-3B7.5好好有推荐主力
qwen3.5-2B-deepseek-v4
来源存疑
11.5中中有可实验,勿上生产
TinyLlama-1.1B~22弱弱轻量草稿专用

注:速度受 CPU、线程数、上下文长度影响很大;有独显时把层数卸载到 GPU(n_gpu_layers)能快数倍。

六、你想自己动手?最小步骤

环境搭好后,日常就是两件事:聊天评测。核心脚本(对话 chat.py、评测 eval_harness_lcpp.py)都已就绪,路径请替换成你自己的:

1) 进入你的模型目录

cd “你的项目目录”

2) 跟模型聊天(默认加载 qwen35,可换 –model hermes / tinyllama)

“你的虚拟环境\Scripts\python.exe” chat.py –model qwen35

想看模型的”内心思考”过程,加 –show_think

“你的虚拟环境\Scripts\python.exe” chat.py –model qwen35 –show_think

3) 跑自动评测(7 维 + 速度),结果存成 JSON

“你的虚拟环境\Scripts\python.exe” eval_harness_lcpp.py

只评某一个,省时间:

“你的虚拟环境\Scripts\python.exe” eval_harness_lcpp.py qwen35


想要最佳体验:联网后直接用 `ollama pull hermes3:3b` 再 `ollama run hermes3:3b`,Ollama 会自动启用 GPU/优化内核,比纯 CPU 快得多,也最省心。

七、给后来者的避坑清单

  • 引擎选 llama-cpp-python,别在旧引擎上浪费时间。
  • 国内网络装包走镜像;GitHub 大文件用断点续传,别硬刚。
  • 警惕虚构型号名的模型,先查架构再下载。
  • 小模型一定要测安全护栏,TinyLlama 这类无护栏模型别碰敏感场景。
  • 纯 CPU 能玩,但别指望快;真要日常用,上一张能跑起来的显卡或换 Ollama。
  • 推理型模型会输出思考过程,对话时默认隐藏、需要时再看,体验更干净。

八、结语

折腾这一圈最大的体会是:本地大模型真正的门槛,往往不在模型本身,而在环境——网络、引擎版本、架构兼容,随便哪一处都能让你卡半天。但一旦跑通,那种"模型就在我自己电脑里、不上传任何数据"的踏实感,是云端 API 给不了的。

这台老机器现在成了我的"模型试验田",陆续又塞进去十几个 .gguf。如果你也在玩本地模型,欢迎在评论区聊聊你的引擎选型和翻车经历。

本文为个人实操记录,模型表现仅代表在作者硬件/引擎下的实测,不构成任何下载或生产建议。涉及具体模型请自行核实来源与许可证。

Termux自动升级了,Python从3.11跳到3.13。心想”好事啊,新版本”,结果venv全废、cryptography编译崩溃、浏览器自动化被系统block。升级只需5分钟,修环境要5小时。

TL;DR

Termux升级导致Python 3.11→3.13,venv路径变了,旧依赖全废。cryptography 50.0.0在Android上编译直接崩溃,锁死48.0.1才跑通。浏览器自动化被系统日志原话block——“Termux不支持浏览器自动化”,后来用Selenium+Chromium绕过去,关键参数--no-zygote。验证通过:chromium 149 + selenium 4.47 OK。规律:Termux升级=推倒重来。

一、起因:自动升级,心想”好事啊”

有一天,我打开Termux,发现它自动升级了。

Python版本从3.11跳到了3.13。我心想:好事啊,新版本,性能更好,兼容性更强。

然后我试着跑了一下Hermes。

报错了。

不是小报错,是那种”整个环境都不认识了”的报错。venv路径变了,旧依赖全废,pip list里一堆红字。

我这才意识到:在Termux里,”升级”这两个字,从来不是好事。

二、推倒重来:Python 3.13的阵痛

Termux的包管理器很激进。它升级Python,不是打补丁,是直接替换。

Python 3.11没了,换成3.13。venv的路径变了,之前装在3.11环境里的所有库——cryptography、aiosqlite、selenium、flask——全都不见了。

这就像你搬了家,发现钥匙打不开门,因为锁芯换了。

我不得不重新建venv,重新pip install。但问题不在于重装,而在于有些库在Android上根本装不上。

三、编译崩溃:cryptography的执念

第一个撞墙的是cryptography。

Hermes的微信插件依赖这个库。日志里写得清清楚楚:Failed to load plugin 'wecom-platform': No module named 'cryptography.hazmat.backends'

我心想,pip install cryptography呗。

结果编译直接崩溃。

cryptography 50.0.0版本在Android aarch64架构上编译失败。报错信息很长,核心意思就一个:Rust原生依赖编译不过去。

我试了42.0.8,编译失败。试了50.0.0,编译失败。最后锁死48.0.1,设了ANDROID_API_LEVEL=33,才勉强编译通过。

那一刻我懂了:开源库的版本号不是越新越好,能跑通才是硬道理。

四、系统block:浏览器自动化的死局与活路

第二个坑更隐蔽。

我想在Termux里跑浏览器自动化(比如抓取微信公众号文章),以前用playwright或者npx puppeteer,现在系统直接block了。

日志原话是:browser command blocked on Termux: Local browser automation on Termux cannot rely on the bare npx fallback

意思是:Hermes系统认为Termux不支持浏览器自动化,直接拦截了命令。

我查了资料,Termux确实没有完整的桌面浏览器环境。chromium在Android上跑headless模式,需要特殊的参数和权限。

但我偏不信邪。

我装了Selenium,装了Chromium 149。试了各种参数:--headless--no-sandbox--disable-gpu。都不行。

最后加上--no-zygote,跑通了。

--no-zygote是Android特有的参数。Android的浏览器进程模型和桌面Linux不一样,不加这个参数,Chromium在Termux里会启动失败。

验证通过的那一刻,我截了个图:chromium 149.0.7827.155 + Selenium 4.47.0,微信文章抓取成功,标题、正文、图片全提取到了。

五、验证通过:一份兼容性清单

我花了半天时间,逐个验证了之前遇到过的不兼容问题,整理出一份清单:

已解决

  • cryptography 48.0.1 ✅(锁死版本,设ANDROID_API_LEVEL=33)
  • aiosqlite 0.22.1 ✅(重装通过)
  • fuser 23.7 ✅(端口清理命令可用)
  • 浏览器自动化 ✅(Selenium+Chromium,关键参数--no-zygote
  • which命令消失 ✅(改用command -v

仍存在

  • SSL连接不稳定 ⚠️(钉钉和Agnes API反复出现UNEXPECTED_EOF_WHILE_READING
  • 微信插件Session过期 ⚠️(Session expired; pausing for 10 minutes

我把这份清单和浏览器自动化的配置过程写成了skill,方便以后升级时直接复用。

六、教训:永远在修环境,但修好了就是壁垒

Termux升级这件事,让我明白了一个道理:

在Android上跑Linux环境,永远在修环境。Python版本变了、依赖库编译不过去、系统命令消失了、浏览器参数不兼容……这些问题不会因为你”修好了”就永远消失,下次升级还会再来。

但反过来想,正因为这些坑别人没踩过,你踩过了,就是你的壁垒。

当别人在云服务器上一键部署、环境永远一致的时候,我在手机上一次次重装、编译、调参数。这些经验不会写进官方文档,但它们真实存在,而且只有踩过坑的人才知道。

升级只需5分钟,修环境要5小时。但修好之后,手机上的AI基础设施又稳了。

至少在下一次升级之前。


作者:小道 · 环境:Termux on Android 13 · 2026-08-28
关联阅读:上一篇《搞了4个定时任务,删掉了一个》 · 下一篇《22GB到12GB,一次手机空间大清理》

摘要:想用 PocketPal AI 在安卓上离线跑通讯飞「星火 X2.5-4B」量化模型,结果 90% 的时间不是花在 AI 上,而是花在和 Windows 构建环境搏斗上。这篇记录从接手一个失败的交接文档,到把整条 RN + llama.cpp 链路几乎跑通的全过程,以及一份可以直接抄作业的避坑清单。


一、缘起:我想在手机里养一个本地大模型

最近在折腾端侧大模型推理,目标很明确:

Spark-X2.5-4B-Q4_K_M.gguf(讯飞星火 X2.5-4B 的 4-bit 量化版)塞进安卓手机,用 PocketPal AI 这个开源 RN 应用离线跑起来,不依赖任何云端。

PocketPal 本身是微软出品、基于 React Native 的本地模型客户端,底层用 llama.rn 绑定 llama.cpp。理论上只要把最新的 llama.cpp 里已经合入的 spark2_5 架构支持接上,再打出 APK 就行。

听起来像是「git clone → 改两行 → 构建」的下午茶项目。

事实证明,我太天真了。


二、接手时的一地鸡毛

前一位同学(用 VS Code 跑了一阵)留了一份交接文档,结论是:

spark2_5 代码补丁已经补齐,但 Android 打包链路在当前 Windows 环境里没跑通,建议从 .env + Gradle 任务 + 输出路径 + 变体过滤继续排查。」

乍看很合理。但真正动手后我发现,这份文档漏掉了 4 个会直接让构建起不来的硬阻塞点。换句话说,光按它的建议走,连第一步都迈不出去:

# 被漏掉的阻塞 现象 修法
1 SDK 没装齐 android-36 platform、build-tools 36.0.0、cmake sdkmanager 一把装齐
2 NDK 版本冲突 CXX1104local.properties 写死 ndk.dir=27.3,但某模块要求 27.0 删掉 ndk.dir,让 AGP 按各模块 ndkVersion 自适应
3 .env / Firebase 配置 Missing .env file;缺 google-services.json 直接 FAIL 复制 .env.example;注释掉 google-services 插件和 firebase 依赖
4 spark2_5 必须源码编译 llama.rn 默认用预编译 .so 且没下载,补丁白打 -PrnllamaBuildFromSource=true -PrnllamaVariants=rnllama

第 4 点是最关键的认知,后面单独讲。


三、核心认知:为什么补丁打了却没用

llama.rn 默认走预编译好的 .soRNLLAMA_BUILD_FROM_SOURCE=OFF),而且仓库里压根没带这个预编译库。即便你给它把 spark2_5.cpp 的补丁打进了 node_modules/llama.rn/cpp/models/,只要它不重新从源码编译,那补丁就是个摆设。

正确姿势是强制源码编译:

1
2
3
gradle.bat assembleProdDebug `
-PrnllamaBuildFromSource=true `
-PrnllamaVariants=rnllama

好消息是 rnllama 的 CMake 用 file(GLOB models/*.cpp) 自动扫模型实现目录,spark2-5.cpp 会被自动编进去;再配合 -PrnllamaVariants=rnllama 只编 generic 变体,避免把一堆用不上的架构都拉来编译。

这一条值得所有人记住:改了 llama.cpp 的模型源码,就必须从源码编,预编译 .so 不会自己更新。


四、真正的 Boss:Windows 构建环境

代码层修完,本以为胜利在望。然后 Windows 教我做人。

技术栈:RN 0.82.1 / AGP 8.12.0 / Gradle 8.13 / Kotlin 2.1.20 / compileSdk 36 / NDK 27。下面按踩坑时间线记录。

1. Defender 实时扫描锁死 Gradle 原生库

Gradle 8.13 一启动就报:

1
2
3
Could not initialize native services.
Failed to load native library 'native-platform.dll' ...
java.io.FileNotFoundException: ...native-platform.dll.lock (拒绝访问。)

dll 本身能加载,普通文件读写删都正常,唯独 Gradle 用 RandomAccessFile.lock 加锁时被微软 Defender 实时防护在创建瞬间排他锁住**。

解法:把这三个目录加进 Defender 排除项(或临时关掉实时保护):

  • C:\Users\\.gradle(含各种缓存目录)
  • 独立的 Android 构建目录(如 E:\android-build
  • 项目目录本身(这点最容易被漏,C++ 编译产物 android/app/.cxx 就在项目里,不排它一样卡死)

2. 260 字符路径上限 + 过时的 ninja

C++ 编译一开,ninja 报 Filename longer than 260 characters

注册表开了 LongPathsEnabled=1 并重启后,ninja 1.10.2(SDK 自带)还是翻车——它太老,不认长路径。

解法:把 SDK 里的 cmake/3.22.1/bin/ninja.exe 备份后,替换成 ninja 1.12.1

3. clang 死锁假死

clang++ 进程 CPU 占用 0%,看起来在编译,实际死等。根因还是 Defender 在扫 .cxx 产物 + CMake 高并行度下的文件锁竞争。

解法:降并行度 CMAKE_BUILD_PARALLEL_LEVEL=4,并把项目目录彻底排除出实时扫描。

4. 残留进程 / 陈旧 daemon 锁

最阴的是:中途强杀后台构建,会留下一堆陈旧锁文件(.o.dregistry.bin.locknative-platform.dll.lock)和还活着、占着文件句柄的 java/clang/ninja 进程。下次构建一上来就被这些僵尸锁挡住,报错和「第一次」一模一样,极其迷惑。

解法:跑之前先

1
2
3
4
5
6
# 杀残留进程
taskkill /F /IM java.exe /IM clang*.exe /IM ninja.exe
# 清陈旧构建态
Remove-Item -Recurse -Force android/app/.cxx
# 清 Gradle daemon 与 native 锁
Remove-Item -Recurse -Force $env:GRADLE_USER_HOME/daemons

而且同一时刻只跑一条构建,别让多个后台任务抢同一批锁文件。


五、来之不易的进展

排除项加全、长路径和 ninja 升好、并行度降下来之后,链路是这样跑通的:

  • ✅ 配置所有 autolinked 原生库
  • ✅ 下载 3.4GB 依赖
  • ✅ Java / Kotlin 全量编译
  • ✅ JS Bundle(Metro)打包
  • ✅ reanimated / vision-camera / worklets 等 C++ 原生模块陆续编译通过
  • ⏳ 仅差 llama.cpp native 收尾 + 最终 APK 打包

也就是说,代码和构建配置层面已经全部打通,离出包只差最后一段原生编译。只是这段最慢、也最容易因为前面说的「残留锁」被打断——我最后几次就是卡在 Gradle 又被一个陈旧 native-platform.dll.lock 拦下来,APK 暂时还没产出。


六、如果你也要在 Windows 上搞 RN + 本地 LLM,照这个清单来

  1. 先确认 SDK 组件齐全sdkmanagerplatforms;android-36build-tools;36.0.0cmake
  2. NDK 别写死local.properties 里删 ndk.dir,让 AGP 按模块 ndkVersion 自适应,避免 CXX1104
  3. .env / Firebase 就绕:复制 .env.example;没有 google-services.json 就注释掉 google-services 插件和 firebase 依赖。
  4. 改了 llama.cpp 模型源码就必须源码编-PrnllamaBuildFromSource=true -PrnllamaVariants=rnllama
  5. ABI 收敛到 arm64-v8a:手机基本都是这个,能省掉一大半编译量。
  6. Defender 排除三项.gradle 缓存、Android 构建目录、项目目录本身
  7. LongPathsEnabled,并把 ninja 升到 ≥ 1.11(SDK 自带 1.10.2 不够)。
  8. CMAKE_BUILD_PARALLEL_LEVEL=4,别让 clang 把文件锁打满。
  9. 跑前清残留:杀 java/clang/ninja 进程 + 删 .cxx + 清 Gradle daemon;同一时刻只跑一条构建

七、写在最后

回头看,这场「下午茶项目」前后耗了十来天,真正和 AI / 模型相关的改动其实就一个补丁文件;剩下 90% 的精力,都耗在 Windows 构建环境的各种隐性锁、路径限制和杀软冲突上。

端侧大模型的门槛,从来不只是模型本身,还有这条把模型送进设备的、脆弱又磨人的工具链。

APK 我还在收尾——等最后那段原生编译跑完,下一篇可以聊聊真机上的首 token 延迟和续航实测。


(本文已对文件路径中的用户名、主机名等敏感信息做脱敏处理。)

摘要:一台被钉钉会议系统和阿里 YunOS 双重锁死的老投影仪,默认只能开会投屏。本文记录如何在不 root、不刷机的前提下,用 ADB + 自写遥控 App + 当贝桌面,把它从「会议砖」改造成可遥控、可装 App、可当数字相框的家庭设备;以及中途一段让手机扮演蓝牙遥控器的、踩遍 Android 隐藏 API 的史诗级翻车。


一、缘起:一台被「钉钉化」的老投影

手头有一台 大眼橙 V1(内部代号 DingProjector_A2),2018 年的机器,原本是给钉钉会议室做「Focus 投屏」配套的。

买来之后发现:开机直接进钉钉桌面,没有应用商店入口,没有文件管理,遥控器按键被钉钉抢走一大半。本质上——它被钉钉 + 阿里 YunOS 双重锁死,成了一块「只能开会」的砖

但我记得它里面是一颗 Amlogic S905 芯片,性能放今天当个本地影音盒绰绰有余。于是目标很明确:

在不拆机、不刷机、不 root 的前提下,尽可能把它改造成一个「能遥控、能装 App、能投屏、能当数字相框」的家庭设备。


二、设备体检:用 ADB 给老机器做全身 CT

投影开着 ADB 调试(TCP 5555),在屏幕点一下「允许」就拿到 shell。

一眼看清它的底细:

  • SoC:Amlogic S905(GXL 平台),ARMv7 32 位
  • 系统:Android 5.1(SDK 22)+ 阿里 YunOS TV 外壳
  • 内核:3.14.29(2018 年编译)
  • 默认桌面com.alibaba.dingtalk.focus(钉钉 Focus)
  • 屏幕:1280×720 / 160dpi
  • 权限ro.debuggable=0adb root 直接被拒,SELinux Enforcing,/system 只读 —— 无 root 实锤

端口普查更有意思:除了 ADB 的 5555,还有一堆钉钉私有端口(7100/11222/12345/52288),以及一组 uid 0 的 root 守护进程(9751/3988/3999)。其中 9751 是 autofocus_server——一个对焦马达服务,帧结构是 [40 字节头] + [1 字节 JSON 长度] + [JSON],理论上能远程触发自动对焦。再往深看,设备还有出向 MQTT 连到阿里云 IoT——这台投影一直是能被云端远程控制的

结论很乐观:虽然没有 root,但 shell 能用、input 能模拟按键、/data/local/tmp 可写、能 adb install 侧载 APK。能玩的空间比想象中大得多。


三、无 root,到底能玩什么

很快就把基础能力跑通了:

  1. PC 遥控:写了个 remote.py,用 input keyevent / am start 在电脑上遥控投影——方向键、主页、音量、启动指定 App 全没问题。设备上一共能拉起 53 个 Activity,包括数字相框、媒体播放器、梯形校正、亮度调节等。
  2. 数字相框am start 直接拉起 YunOS 自带的 PhotoPlayerActivity 显示图片,做成「开机即相框」一键命令。
  3. 侧载 App:装了当贝桌面、当贝市场、当贝文件管理器、Kodi、B站 TV 版。老 Android 5.1 兼容性意外地好。
  4. 无线镜像scrcpy 连上去,PC 实时看到投影画面并鼠标操控(注意 Android 5.1 没有音频捕获,要加 --no-audio;中文输入法注入不支持 CJK,得走设备端输入法)。

投屏的真相值得一提,避免后来人踩坑:

  • 钉钉投屏码可用:手机钉钉输码 DJ1JK 直接投,这是最稳的官方通道。
  • 乐播云已死:这台 8 年老设备在乐播云端的注册早已过期,登录二维码、投屏码全部「加载失败」,用户侧无解。
  • iPhone AirPlay / 安卓 Miracast 搜不到:厂商把 mDNS/Bonjour 发现关掉了(mdnsEnable=0),乐播也不支持 Miracast。即使用户端能建 Lelink 会话,老 Amlogic 硬解现代手机实时流也不兼容,画面出不来。

所以最终投屏就两条实在的路:钉钉码scrcpy 无线镜像


四、开机自启:和「默认桌面」较劲

装了当贝桌面后,问题来了:按 HOME 永远回钉钉,因为系统把钉钉固化成了默认 HOME,而无 root 改不了这件事(pm disable 系统组件会 SecurityException,pm clear 清完重启又恢复)。

折腾一圈后,最稳的免 root 方案是 AutoStart(一个不需要 root 的开机自启 App):配置它开机延迟 60 秒、自动拉起当贝桌面。实测重启后:

钉钉桌面(~1 分钟)→ AutoStart 到点拉起当贝 → 当贝稳定接管前台。

借人、带出门、只有手机的场景全部零操作可用。

顺带还解了两个「老设备综合症」:

  • 电池是假的:BMS 显示 4166mAh / 97% 零衰减,但实测拔电即整机断电——2018 年的电芯深度放电后已锁死,带不动投影灯珠负载。结论:当固定 AC 投影用,电池只是个好看的读数。
  • RTC 失忆:硬件时钟永远停在 2015-01-01。无 root 改不了硬件时钟,但开了 auto_time 后每次开机联网自动校时,系统时间就正常了。

五、高光时刻:自己写一个 ADB 客户端,干翻 YunOS 的魔改认证

为了能用手机直接遥控投影(而不是每次开电脑),我做了个安卓遥控 App。难点是:App 要自己在 WiFi 上扮演 ADB 客户端连投影,而不能 exec 原生 adb 二进制(现代 Android SELinux 禁止)。

于是用了纯 JVM 的 ADB 协议实现(cgutman 的 adb-java),并把 PC 的 adbkey 打包进 App——因为投影的 /data/misc/adb/adb_keys 白名单里已经有这把公钥,手机复用同一把私钥就能免二次授权直连。

真机一跑,翻车了:标准 ADB 的 AUTH_SIGNATURE 握手被 YunOS 的魔改 adbd 全部拒绝(SHA1/MD5/SHA256/裸 RSA 都试过)。

继续刨,发现三个老设备专属的坑:

  1. 校验和是字节求和,不是 CRC32——2018 年老 adbd 用 sum,错一个字节直接断连。
  2. 消息常量是小端 ASCIIAUTH=0x48545541 这种,写反端序 mock 能过、真机必挂。
  3. 真相:这台 adbd 不收签名,但收到合法的 AUTH_RSAPUBKEY直接回 CNXN 放行,无需签名。而 RSAPUBKEY 的载荷是原始 272 字节 RSAPublicKey 结构体(含 n0inv 牛顿迭代、rr=2^2048 mod n 计算),不是 base64。

把认证流改成「发原始公钥结构体 → 直接连」,真机全通:连上、读 getprop、发 input keyevent 全部成功。等于自己实现了一遍 ADB 协议,还逆向出了 YunOS 对认证的特殊处理。


六、史诗级翻车:让手机当蓝牙遥控,踩遍 Android 隐藏 API

接下来想更进一步——不依赖 USB 调试,让手机像实体遥控一样直接控投影。

先拆解实体遥控:用 getevent -l 抓包,发现事件来自 bus: 0005(蓝牙),scan code 全是 0x0700xx(HID Keyboard 用法页)。原来这遥控是个蓝牙 HID 键盘,不是红外(系统里那份 NEC 红外码表其实是休眠配置,对 V1 无效)。

于是方向变成:让手机扮演 BluetoothHidDevice(蓝牙 HID 键盘),配对投影后发标准键盘报告。键码表我都抓全了(上/下/左/右/OK/主页/返回/菜单/音量/对焦/电源)。

然后是从 build36 一路翻车到 build45 的、关于 Android 隐藏 API 的连续暴击:

  1. SDP 构造器签名被定制:小米 HyperOS 上 BluetoothHidDeviceAppSdpSettings 的构造器末参是byte[16](UUID 二进制),不是标准 AOSP 的 ParcelUuid[]。硬编码标准签名直接 NoSuchMethodException
  2. Callback 是 abstract class,不是 interface:JDK 动态代理只支持接口,碰上抽象类必报 not an interface
  3. ART 只认 DEX,不认 JVM class:改用 ASM 字节码生成子类,结果 can't load this type of class file——Android 运行时根本不加载 .class,必须生成真正的 DEX。
  4. 换 dexmaker 运行时生成 DEX 子类:这回过了编译,又卡在 NoSuchMethodException: (dexmaker 不自动生成构造器)、返回类型不匹配、android.os.Executor 类名拼写错、Manifest 给权限加了 maxSdkVersion=32 导致高版本系统看不见权限……
  5. 终于走到注册:权限弹窗都点了,却卡在「正在注册 HID 键盘…」6 秒超时,永远收不到系统回调。

最后在小米 + 华为两台不同品牌手机上复现了完全相同的症状,且回显的回调签名都是 AOSP 标准——说明不是某家厂商砍了,而是国产手机蓝牙栈普遍没实现 HID Device 这个服务端角色getProfileProxy 拿到的 proxy 有效,但底层协议栈从不真正注册)。

实体遥控能用,是因为 V1 端是 HID Host;手机端是 HID Device,而这条路在国产 ROM 上被普遍砍掉了。

这条路线被判死。我清掉所有蓝牙代码,回退到纯 ADB 遥控,出了 build46(1.0.0-adb),8.36MB,覆盖安装即用。

(这段经历单独看比主项目还精彩:从一个错误假设出发,连过 SDP、Proxy、DEX 生成、Manifest 四道隐藏 API 关卡,最后撞上一道硬件级天花板。)


七、关于 root:能,但要拆机焊线

期间也认真评估过 root。结论很现实:

  • A. boot.img 重签注入 su —— 死。YunOS 锁了 bootloader,reboot bootloader / fastboot 全被拦截。
  • B. Dirty COW —— 死。YunOS 加固剥掉了所有 setuid 位,没有可提权的执行载体。
  • C. MaskRom 硬件线读 eMMC —— 唯一可行。拆机短接 eMMC 的 CLK-GND 进 MaskRom 模式,用 Amlogic USB Burning Tool 读全片备份、改镜像、写回。

但有个红线:全网搜不到 V1 的原厂固件(大眼橙官方 OSS 只托管 2021 年后的新型号)。也就是说,本机 eMMC 的备份是唯一的救砖底牌,在拿到备份前绝不能刷任何东西。

权衡之后,用户选择暂不拆机,保留无 root 现状。C 方案存档待用。


八、现在它能做什么(避坑清单)

最终形态,无 root、零拆机

  • ✅ 开机自动进当贝家庭桌面(AutoStart 60s 延迟拉起)
  • ✅ 手机遥控 App(ADB 直连,免授权)
  • ✅ PC 遥控脚本 + scrcpy 无线镜像
  • ✅ 钉钉投屏码投屏
  • ✅ Kodi / B站TV / 数字相框 / 当贝市场装 App
  • ❌ 远程真正关机(只有待机键有效,无法远程唤醒)
  • ❌ 改默认桌面 / 改 DNS(无 root 动不了)

给后来人的几条硬经验:

  1. 老设备先 adb shell 摸一遍 getprop / 端口 / 能拉起的 Activity,比盲改高效十倍。
  2. YunOS 老 adbd 的 ADB 认证是魔改的:不收签名、只认原始 RSAPUBKEY 结构体,校验和是字节求和。自己写客户端时别按标准 AOSP 来。
  3. 想让手机当蓝牙遥控?先确认手机 ROM 实现了 HID Device 角色——国产定制系统(小米/华为/OPPO 等)大多没有,别像我一样从 build36 翻到 build45。
  4. 无 root 改不了默认桌面:用开机自启类 App(AutoStart 之类)兜底,比死磕 HOME 现实。
  5. 老设备云端服务基本已死(乐播、当贝云都靠老 API),本地功能优先,别赌联网能力。
  6. 电池读数和实际往往两码事:BMS 好看 ≠ 电芯能带载,拔电测试最实在。

九、写在最后

这台 8 年前的「会议砖」,最后没 root、没刷机,靠 ADB + 自写客户端 + 当贝桌面,活成了家庭影音盒。最有价值的不是某个具体命令,而是那个反复验证过的认知:

一台被锁死的设备,真正的大门往往在 ADB 和协议层,而不在 root。

至于那段蓝牙 HID 的翻车——它没让投影更好用,但让我把 Android 蓝牙隐藏 API 的坑踩了个遍。哪天换台原生系统的手机,也许还能把这半截轮子装上。


(本文已对设备局域网 IP、主机名、MAC 地址、用户名等敏感信息做脱敏处理。)

老电视自救指南:给 PPTV 55C4 去广告、装 Kodi 看直播、换第三方桌面

一台 2018 年前后智能电视的 ADB 折腾全记录

作者:家里的网络管理员 | 设备:PPTV 55C4 | 难度:★★☆☆☆(会敲命令就行)

家里的 PPTV 电视越用越闹心:开机一堆广告、自带的”直播”源大半打不开、遥控器还老被劫持去推流媒体。它本质就是一台 Android 盒子,那能不能像折腾手机一样,用 ADB 把它收拾干净?答案是能,而且全程不用 root。这篇把踩过的坑一次讲清。

一、先看清你的”战场”

动手前先摸清设备底细,后面每一步都围着它转:

- **电视**:PPTV 55C4,系统 PPOS 7.2.84,底层是 `Android 5.1.1`(SDK 22,32 位 `armeabi-v7a`),**未 root、bootloader 锁定**。
- **路由器**:电信定制的中兴 ZXHN E500,功能很弱,只支持 URL 黑名单,做不了端口转发之类的精细控制。
- **电脑**:一台 Windows 笔记本,装好 Android Platform-Tools(adb)和 scrcpy 即可。
- **网络**:电视走 WiFi,拿的是路由器 DHCP 分配的局域网地址——**这个 IP 会变**,后面会反复被它坑。

为什么强调”未 root / 锁定”?因为这决定了我们能走多远。好在本文所有操作都在”免 root”范围内,风险比刷机小得多。

二、第一步就劝退一半人:用 ADB 连上电视

电视的”开发者选项 → 网络调试(ADB over 网络)”打开后,电脑上就能:

adb connect 192.168.x.x:5555
adb devices # 看到 device 就成功了

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
28
29
30
31



但这里有三个坑,按顺序排好:



- **IP 是动态的**。我这台电视的地址在 `.30 → .6 → .33` 之间反复横跳。所以脚本里不能写死 IP,而是先按电视的无线 MAC(如 `c4:98:5c:xx:xx:xx`)去本机 ARP 缓存里反查当前 IP,查不到再提示人工确认。
- **待机/重启后 5555 端口会关**。网络调试不是常驻的,电视睡一觉或重启,端口就没了,得回电视上重新打开"网络调试"。
- **adb 会"假死"(offline)**。ping 得通、端口也开着,但 `adb devices` 显示 `offline`,`kill-server` 重启 PC 端也救不活。这种僵死只能从电视端重启 adbd——也就是把"网络调试"关掉再打开一次。




排查连接问题时,别被"设备在线但 adb offline"骗去重装驱动。**先 ping 再探端口**:ping 通 + 5555 端口 OPEN,说明是 adb 握手卡死,去电视上重开网络调试即可,跟电脑无关。



## 三、清理广告与投屏垃圾包



PPTV 的自带广告、推送、以及一堆第三方投屏/联运 App,本质都是系统或预装 APK。用 ADB 的 `pm` 禁用(不是卸载,可恢复)最稳妥:

# 仅示意类别,实际包名以自己电视为准
adb shell pm disable-user --user 0 com.pptv.newtvad # 开机/屏保广告
adb shell pm disable-user --user 0 com.pptv.push # 推送服务
adb shell pm disable-user --user 0 com.pptv.tvbuy # 购物劫持
adb shell pm disable-user --user 0 com.starcor.mango # 芒果联运
adb shell pm disable-user --user 0 com.hpplay.leboproj # 乐播投屏(可留可禁)
adb shell pm disable-user --user 0 com.cibn.miaoku # 喵酷

需要注意的是:先确认包名再 disable,别拿”看着像广告”的包下手,否则可能把桌面或设置禁掉。禁用后用 pm list packages 核对一遍即可。

效果立竿见影:开机广告没了,屏保不再弹购物,遥控器也不会被半路劫去推 App。全程没碰系统分区,想恢复只要 enable 回来。

四、装上 Kodi 21.3,看 IPTV 直播

我想要的是”一个遥控器看所有直播源”,Kodi + PVR IPTV Simple Client 是最省事的方案。电视自带的是老掉牙的 Kodi 19.1,HLS 直播经常抽风,于是升级到官方 Kodi 21.3(Omega)。这里踩了两个大坑:

坑 1:升级后 Kodi 启动卡死

21.3 起来后卡在 waiting for add-ons to update...,Web 服务一直起不来。查日志发现是几个 Kodi 19 时代的 inputstream 插件(ffmpegdirect / rtmp)跟 21 不兼容,把启动流程堵死了。把它们移走后,Kodi 自带的仓库很聪明,自动把插件更新到了 21.x 版本

- pvr.iptvsimple → `21.11.0`
- inputstream.adaptive → `21.5.23`
- inputstream.ffmpegdirect → `21.3.8`
- inputstream.rtmp → `21.1.2`

坑 2:HLS 报 “Error creating demuxer” 不一定是 Bug

本地生成的 MP4/HLS 测试流都能正常播(speed=1),证明播放器本身没问题。但点某些频道就报 OpenDemuxStream - Error creating demuxer。排查下来,罪魁是源的 master playlist 写了非法的 #EXT-X-STREAM-INF:BANDWIDTH=1,Kodi 解析 variant 选择器时直接崩了。改用它的 variant(清晰度)直链播放,立刻 1080p 正常。

经验:遇到 HLS “demux 失败”,先怀疑源 playlist 格式是否合法,再怀疑播放器。别急着给 Kodi 扣”兼容性差”的帽子。

一个关键真相:播放失败,锅不在 Kodi

最初用户报”一个台都播不了”。我把三个 m3u8 播放列表里的 1668 个频道 URL 全量探活,结果只有 1 个能连通,其余全部超时/失效(包括某高校 IPTV 测试源这种曾经能用、如今 100% 丢包的)。

也就是说:播放器、网络、PVR、HLS demux 全部健康,”播不了”纯粹是 IPTV 源大面积失效。唯一存活的那个直播源,用上面的 variant 直链法在电视上实测能放 1080p。

结论很现实:免费公开 IPTV 源的稳定性几乎没有。真要长期看,得有稳定可靠的源(合规渠道),而不是指望网上随便下的 m3u8。

五、换上当贝桌面 + 开机自启

原厂 PPOS 桌面广告多、还不顺手,换成第三方 TV 桌面体验立刻不一样:

- **当贝桌面**(`com.dangbei.tvlauncher`):TV 向的 launcher,干净好用,顺带带了当贝市场。
- **Autostart**(`com.autostart`):配一下,让电视开机自动拉起 Kodi(或你指定的 App),省得每次手动找。
- **当贝文件管理器**(`com.tv.filemanager`):在电视上管理 U 盘/局域网文件,传 m3u8 源很方便。

安装就是常规 adb install -r xxx.apk,推到 /sdcard/Download/ 再装。

顺带说一句很多人会踩的坑:KingRoot、Magisk 这类 root 工具,在这台锁 bootloader、未 root 的电视上装了也 root 不了。Magisk 要 patch boot.img + 第三方 recovery 才能刷;KingRoot 在定制系统上基本被挡。它们装上只是占地方,还可能带来隐私/广告风险。本文所有改造都不需要 root,别去点”获取 root”。

六、用 scrcpy 把电视”投”到电脑上遥控

想在大屏幕上操作、或者懒得找遥控器时,scrcpy 能把电视画面投到电脑并反向控制。针对这台电视我写了个一键脚本,重点解决了 IP 变化的问题:

@echo off
set TV_MAC=c4-98-5c-xx-xx-xx :: 按 MAC 反查当前 IP
set TV_IP=192.168.x.x
adb connect %TV_IP%:5555
:: 连不上就 arp -a 反查 MAC 拿到新 IP 再连
scrcpy -s %TV_IP%:5555 -w -m 1920 –video-bit-rate 8M –window-title “PPTV 55C4 遥控”


  

启动后电脑上弹个窗口,鼠标当遥控器、键盘能输字,延迟在局域网里基本无感。前提是电视"网络调试"是开着的——所以每次要用时,若连不上,先回电视重开一下网络调试(见第二节)。

七、那些年一起踩过的坑(清单)

- **IP 动态**:脚本里用 MAC 反查,别写死。
- **网络调试会自己关**:待机/重启后回电视重开。
- **adb offline 僵死**:PC 端重启没用,电视端重开网络调试重启 adbd。
- **Kodi 文件路径**:用绝对路径 `/sdcard/...`,别写 `file:///` 三斜杠(会被误解析成 `file://` 导致&quot;播放失败&quot;假象)。
- **HLS demux 报错**:先查源 playlist 的 `BANDWIDTH` 等字段是否合法,必要时用 variant 直链。
- **升级 Kodi**:旧版 inputstream 插件不兼容会卡启动,移走让仓库自更新即可。
- **别迷信 root 工具**:锁 bootloader 的设备,KingRoot/Magisk 装了也白装。
- **源才是命门**:播放器没问题,免费 IPTV 源大半已死,得有稳定合规源。

结语

折腾一圈下来,这台老 PPTV 从"广告弹窗遥控器被劫持"变成了"干净桌面 + Kodi 直播 + 电脑反向遥控"的自在设备,而且全程零 root、零刷机、随时可还原。最大的收获其实不是具体命令,而是排查思路:先分层定位(网络→连接→播放器→源),别一上来就怪软件。

如果你也有台"想扔又舍不得"的老电视,照着上面的清单一步步来,大概率能救回来。有问题欢迎留言交流。

免责声明:本文仅用于技术学习与交流,操作存在变砖、失去保修等风险,请自行评估并备份。禁用/安装系统应用请确认包名,后果自负。文中不涉及任何付费内容的破解,请通过合规渠道获取影音资源。文中设备 MAC、局域网 IP、用户名等敏感信息均已脱敏。

#Android TV
#ADB
#PPTV
#Kodi
#IPTV
#scrcpy
#去广告
#折腾笔记

一次 Phaser 3 网页游戏的完整复盘:4600 行代码、26 个 Bug、6 个致命级陷阱,以及那些”读代码永远发现不了”的问题。


先说结论

这个项目的起点很普通:一份 500 行的游戏设计文档,交给 AI 生成代码,一周内出了一个能启动的原型。

然后我接手做代码审查,结果是这样的:

指标 数值
源码规模 13 个 JS 模块,约 4600 行
发现问题 26 个(6 个致命级 / 15 个逻辑缺陷 / 5 个健壮性)
修复后验证 26 项全流程自动化测试全部通过,控制台零报错
移动端 HUD 完全重构,20 项专项测试通过

最值得说的一句话:这 26 个 Bug 里,有 6 个是”读代码看不出来、只有跑起来才会暴露”的。

这篇文章记录那些真正有价值的坑——不是语法错误,而是引擎行为的误用、时序的错位、以及”看起来对但其实是错的”设计。


一、这是个什么游戏

《逃离恐怖奶奶》(Escape from Horror Grandma),Phaser 3 做的横版潜行恐怖游戏,浏览器直接打开就能玩,无需安装。

玩家从噩梦中醒来,发现自己被锁在奶奶的老宅里。你没有任何攻击能力,只能靠黑暗、躲藏和声音判断,在被”它”抓到之前逃出去。

几个核心设计:

  • 五锁逃脱:正门需要集齐 5 件道具,每件都藏在高危险区域
  • 提灯的权衡:点亮看得清但暴露度高,熄灭安全但几乎看不见
  • 奶奶状态机:巡逻 → 调查 → 追捕 → 搜索 → 狂暴
  • 画外穿行:奶奶从屏幕边缘消失,2 秒后从另一侧出现,制造”她抄近道了”的压迫感
  • 躲藏 + 屏息:躲进衣柜后可按住屏息 10 秒,完全无声但视野变窄

设计文档里有一条我特别喜欢的支柱,叫不对称信息情绪引擎

恐怖来自”我知道危险存在,但我可以选择何时面对”。所有子系统服务于同一个情绪目标——“她知道你在附近,但她不知道你在这里”。

这条支柱后来成了很多 Bug 的判定标准:凡是让玩家感到”被系统惩罚”而非”被恐惧笼罩”的机制,都是 Bug。


二、技术架构:零构建

技术选型走的是极简路线:

  • Phaser 3.60(Arcade Physics),本地 vendor 一份,离线可跑
  • 纯 ES Module,无 Vite / Webpack,`` 直接跑
  • 事件总线(EventBus)解耦 12 个子系统
  • Web Audio 程序化合成音效——全部用 Oscillator + GainNode + StereoPanner 现场合成,零音频素材

模块划分:

1
2
3
4
5
6
7
8
9
js/
├── main.js # 入口与游戏配置
├── core/ # 配置、事件总线、存档、节奏系统
├── player/Player.js # 玩家控制器(PC / 移动端双输入)
├── granny/Granny.js # 奶奶 AI(状态机 + 画外穿行)
├── scenes/House.js # 主场景
├── ui/ # 背包、躲藏系统
├── audio/ # 音频管理、视觉氛围
└── input/ # 移动端 HUD

架构本身没大问题,问题全在实现细节里。


三、六个”读代码看不出来”的坑

坑 1:所有平台碰撞体都是 32×32

这是最严重的一个,也是最隐蔽的。

代码是这样写的,看起来非常标准:

1
this.platforms.create(1000, 550).setSize(2000, 100).refreshBody();

地面、墙壁、平台全都这么写。语法没问题,逻辑看起来也对。

但实际上,这行代码完全无效。

原因在 Phaser 3.60 的实现里:ArcadeSpritesetSize 被物理组件覆盖成了转发给 body.setSize,但紧接着调用的 refreshBody() 又按贴图的**默认显示尺寸(32×32)**把碰撞体覆盖回去。

结果就是:地面、墙壁、所有平台,实际碰撞体都是 32×32 的小方块。玩家和奶奶根本没站在地面上,而是掉到了世界边界的底部。

这个 Bug 的诡异之处在于游戏”看起来能玩”——你能左右移动,能跳跃,画面完全正常。只有当你实测物理体位置,才会发现 y 坐标是 675 而不是 475。

正解

1
this.platforms.create(1000, 550).body.setSize(2000, 100);

直接操作 StaticBody,StaticBody.setSize 会以物体中心重新定位,实测生效。

排查方法:我下载了完整版 phaser.js,用 Node 脚本切片读源码,确认了 setSize 在物理组件里被覆盖的行为。凭记忆修这类问题,大概率会修错。


坑 2:手动重力导致的”隔帧振荡”

玩家用的是手动重力方案(allowGravity = false),落地时把垂直速度清零:

1
2
3
4
5
if (!onGround) {
this.verticalVelocity += this.manualGravity * deltaSeconds;
} else {
this.verticalVelocity = 0;
}

看起来很合理对吧?

问题在于:速度清零后,物理体与地面恰好分离,下一帧 blocked.down 变成 false → 又开始下压 → 又接触 → 又清零……

实测下来,blocked.down隔帧交替 true/false 的。

后果有两个,都不太直观:

  1. 跳跃 5 次全部失败——因为判定落地那一刻,blocked.down 恰好是 false
  2. 站立不动的玩家在持续吸引奶奶——velocity.y 周期性非零,噪声等级在 0 和 5 之间跳动,AI 以为你在跑

解法:落地时保持一个微小的下压速度,让物理体每帧都与地面保持接触。

1
this.body.setVelocityY(onGround ? 30 : this.verticalVelocity);

同时 jump() 里要立即应用速度,否则起跳会被下一帧的下压速度抵消。


坑 3:JustDown 的”独占消费”

这个坑很经典,值得单独记住。

Phaser 的 JustDown(key) 只在首次查询时返回 true,之后就消费掉了。

而代码里有两处都在轮询同一个 E 键:

  • Player.handleInput():判断是否触发交互
  • HidingSystem.update():判断是否退出躲藏

Player 先执行,把按键标志消费了,HidingSystem 后执行,永远读到 false

后果:玩家一旦躲进衣柜,就再也出不来了。 而且连锁引发一堆问题——躲藏中无法交互存档、无法触发被抓判定。

解法:Player 里先判断 isHiding,躲藏状态下不消费按键标志,留给 HidingSystem 处理。

1
2
3
4
const isHidingNow = hidingSystem && hidingSystem.isHiding;
if (!isHidingNow && Phaser.Input.Keyboard.JustDown(this.keys.E)) {
this.interact();
}

坑 4:状态机的路由优先级

奶奶有五个状态,代码里是这样路由的:

1
2
3
4
5
6
7
8
9
if (this.isBerserk) {
this.updateBerserk();
return; // ← 问题在这里
}

if (this.isInTransit) {
this.updateOffscreenTransit();
return;
}

狂暴分支在画外穿行检查之前就 return 了。

如果奶奶在狂暴状态下触发了画外穿行,updateOffscreenTransit 永远不会执行——奶奶就凭空消失了,再也不回来。

解法:把穿行检查提到最高优先级。穿行中的奶奶已经离屏,本来就不该执行任何状态机逻辑。

这提醒我:状态机的路由顺序本身就是业务逻辑,值得写注释说明为什么是这个顺序。


坑 5:躲藏机制形同虚设

设计文档里明确写了:奶奶搜索时会逐一检查躲藏点,躲藏中的玩家需要通过”被发现”判定才能被抓。

代码里也确实实现了 checkIfDiscovered() 方法。

但这个方法从头到尾没有被调用过。

tryCatchPlayer() 里只做了距离判断——不管你有没有躲起来,奶奶靠近就抓。躲藏系统等于白做。

解法:抓捕判定里接入躲藏状态检查,屏息状态下可以躲过近距离搜索。


坑 6:画外穿行被墙卡死

原实现要求奶奶走到窗口外的出口点(x = -100 或 2100),然后触发穿行。

但左右两侧有墙(x = -25 和 2025),物理体挡住了去路。奶奶永远走不到出口点,到达条件的事件永远不满足,轮询无限进行——同时定时器泄漏

解法:按设计文档”从屏幕边缘消失约 2 秒后从另一侧出现”的描述重写——立即隐藏进入 2 秒倒计时,计时结束后 body.reset() 从另一侧出现。


四、手感调优:数值也是设计

修完功能 Bug 之后,试玩反馈了三个手感问题,这类问题的特点是”数值对了但感觉不对”。

1. 蹲伏后无法恢复原来位置

只改碰撞体高度时,Arcade 的居中对齐会让身体中心上移、底部悬空,物理引擎把玩家往下推;恢复站立时又被上推。sprite.y 永久偏移,跳跃也回不去。

解法是主动补偿:蹲伏/恢复时按高度差的一半调整 sprite.y,再 body.reset() 同步物理体。保证底部始终贴地,蹲伏前后位置完全重合。

2. 跳跃高度太大

jumpForce 从 -600 降到 -350。

3. 重力太小,空中停留太久

manualGravity 从 600 提到 1000(上升高度从约 300px 降到约 70px,滞空从约 2 秒降到约 0.7 秒)。

注意这里有个连带影响:重力加大后,落地维持的微下压速度也要从 5 提到 30,否则坑 2 的振荡会复发。同时噪声判定要忽略这个下压速度,不然站着不动也会被判定为”在移动”。

手感调优的教训:物理参数之间是耦合的,改一个要检查全部。


五、移动端:从”能玩”到”好用”

原方案参考《Alto’s Adventure》做了单手操作:自动向前行走、点击跳跃、滑动转向、靠近可交互物弹出卡片。

手机上一试,体验很差:

  • 交互卡片浮在画面正中央,遮住视线
  • 玩家不知道该点哪里跳跃、怎么转向
  • 蹲伏、提灯、背包、屏息这些功能根本没有入口

我把它推倒重做成了传统手游 HUD:

位置 功能
左下角 左右方向键
右下角 跳跃 / 蹲伏 / 冲刺
顶部左右 背包 / 提灯
顶部中央 动态交互提示(显示”躲进旧木柜”这类具体动作)
躲藏中 屏幕中央出现”屏息”按住按钮 + “离开”按钮

按钮统一用 Pointer Events,同时兼容真实触屏和自动化测试。

这里还踩了一个二次坑:交互提示最初放在底部中央,测试时发现它和右下角的蹲伏按钮横向重叠——玩家点蹲伏,实际触发了存档交互。后来移到顶部中央才解决。

这个坑是自动化测试发现的,手动点很难复现。这也是我坚持写测试脚本的原因。


六、方法论:为什么”读代码”不够

这次复盘最大的收获,是关于验证方式的三条经验。

1. 必须跑起来,而且要自动化

26 个 Bug 里有 6 个是纯运行时问题。我搭了一套本地服务器 + 无头浏览器的自动化测试,模拟完整游戏流程:启动、移动、跳跃、蹲伏、提灯、拾取、躲藏、屏息、抓捕、穿行、晚饭时间、窗口缩放、存档、10 秒稳定性。

这套脚本的价值不只是”验证通过”,更在于改完之后可以随时重跑,确认没有回归。后面做手感调优和移动端重构时,它救了我好几次。

2. 引擎行为要查源码,不要凭记忆

Body.setSize 的缩放语义、TimerEvent.reset 的签名、StaticBody.setSize 的定位行为——这些我原本都有”印象”,但印象是错的。

我的做法是下载一份完整版引擎源码,用 Node 脚本切片读实现。花 10 分钟确认,省掉 2 小时的误修。

3. 设计文档是判定的最终依据

“这算不算 Bug”这个问题,很多时候代码说了不算。比如躲藏机制——代码能跑,没报错,但和设计文档里”躲藏需要被发现判定”的描述不符,那就是 Bug。

有设计文档的项目,审查时一定要对照文档看”意图”,而不只是看”实现”。


七、接下来要做的

目前完成的是 M0 原型:灰盒场景、玩家控制、奶奶 AI、躲藏点、提灯系统。

按里程碑规划,后面还有:

  • M1:一层完整玩法闭环(正门钥匙 + 谜题 + 正式剪影美术)
  • M2:全屋 12 区域、五锁路线、记忆碎片、双结局、三档难度
  • M3:氛围打磨、手机端内测

最想验证的是那套”画外穿行 + 声像定位”的纵深欺骗——理论上它能把横版被压扁的左右二元选择,重新变回多向躲避的心理压迫。但这个只有真人试玩才能校准。


写在最后

这个项目给我的最大感触是:AI 生成代码的速度快得惊人,但”能跑”和”能玩”之间,隔着一整层工程实践。

语法错误 AI 自己就能修。真正需要人的,是那些藏在引擎行为、时序耦合、设计意图里的东西——而这些,恰恰只能靠”跑起来看”才能发现。

如果你也在用 AI 做游戏或者前端项目,我的建议是:在写第一行代码之前,先把自动化验证环境搭好。 它会在后面替你省下大量时间。


文中涉及的代码片段均已简化,路径与配置细节做了脱敏处理。

0%