<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Saurlax&apos;s Blog</title><description>A blog about web development, programming, and more.</description><link>https://saurlax.com/</link><item><title>TI IWR1642BOOST 毫米波雷达</title><link>https://saurlax.com/blog/iwr1642boost/</link><guid isPermaLink="true">https://saurlax.com/blog/iwr1642boost/</guid><description>最近拿到了一块 TI 的 IWR1642BOOST 毫米波雷达评估板，主要是想看看 77GHz 雷达在室内人员检测和手势识别上能做到什么程度。之前做 WiFi CSI 感知的时候拿到的只是信道状态变化...</description><pubDate>Sun, 28 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近拿到了一块 TI 的 IWR1642BOOST 毫米波雷达评估板，主要是想看看 77GHz 雷达在室内人员检测和手势识别上能做到什么程度。之前做 WiFi CSI 感知的时候拿到的只是信道状态变化，能判断有没有人、大致在哪，但距离和速度都是模糊的。毫米波直接出点云坐标和速度矢量，数据维度完全不在一个量级。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;iwr1642-board.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;毫米波雷达不是什么新技术了，车载领域用了很多年。不过这几年随着 TI 推出集成 DSP 的单芯片方案，原本需要一堆分立器件的方案被压缩到了邮票大小的封装里，成本和体积降下来之后开始往工业安防和智能家居渗透。&lt;/p&gt;
&lt;p&gt;说原理的话，FMCW（调频连续波）体制的雷达跟脉冲雷达思路不太一样。它发射频率随时间线性上升的 chirp 信号，碰到目标反射回来后，接收信号和发射信号混频得到拍频，拍频的值跟目标距离成正比。考过驾照的应该知道多普勒效应，同一距离门上多个 chirp 之间的相位变化能算出目标速度。至于角度，靠的是多个接收天线的相位差，MIMO 体制下 2 发 4 收等效成 8 路虚拟通道，能做方位角和俯仰角的二维分辨。&lt;/p&gt;
&lt;p&gt;这三样东西（距离、速度、角度）在雷达里叫 point cloud，跟激光雷达的点云差不多概念，但多了一个速度维度：毫米波雷达能直接告诉你目标是靠近还是远离，速度快慢。&lt;/p&gt;
&lt;p&gt;IWR1642 内部有一颗 C67x DSP 和一颗 Cortex-R4F MCU，DSP 跑信号处理链（FFT、CFAR、角度估计），R4F 做控制和通信。&lt;/p&gt;
&lt;h2&gt;硬件一览&lt;/h2&gt;
&lt;p&gt;IWR1642BOOST 是 TI 官方的 BoosterPack 评估板，大概巴掌大小：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;芯片&lt;/strong&gt;：IWR1642，76-81GHz，单芯片集成射频 + DSP + MCU&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;天线&lt;/strong&gt;：板载蚀刻天线，2 发 4 收，不需要外接天线就能跑&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;供电&lt;/strong&gt;：5V DC 圆口，要求至少 2.5A，峰值功耗不低&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;调试&lt;/strong&gt;：板载 XDS110 JTAG，一根 Micro USB 线搞定编程和调试&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据接口&lt;/strong&gt;：UART 走 USB 做配置和数据传输，另外一路 CAN 能直接连车载设备。板上还有个 LVDS 口，配合 DCA1000EVM 采集卡才能抓原始 ADC 数据，不接 DCA1000 的话就用 SDK 内置的处理链，DSP 上跑完直接出结果&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;扩展&lt;/strong&gt;：标准 BoosterPack 排针，能叠 TI 的 MCU LaunchPad 做二次开发&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;配件包里给了 Micro USB 线、安装支架、螺丝和跳线帽。唯一需要自己备的是 5V 电源适配器，圆口 2.1mm 中心正极。手边没有的话拿个树莓派的 5V 3A 电源也能顶上。&lt;/p&gt;
&lt;h2&gt;软件工具链&lt;/h2&gt;
&lt;p&gt;TI 给这套雷达板的软件支持分几个层面：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;mmWave SDK&lt;/strong&gt;：核心开发套件，包含驱动、DSP 信号处理库、Demo 源码。开箱 Demo 的预编译固件就在 SDK 里，烧进去直接跑&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;mmWave Demo Visualizer&lt;/strong&gt;：网页版可视化工具，连上串口后能实时看点云、距离多普勒热力图，也能发配置参数给雷达。不需要装任何本地软件，浏览器打开就能用&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;UniFlash&lt;/strong&gt;：编程工具，用来往 IWR1642 的 Flash 里烧固件&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Code Composer Studio&lt;/strong&gt;：如果你要改 DSP 代码或者 R4F 上的应用逻辑，在 CCS 里写。TI 版 Eclipse，该有的都有但启动速度感人&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;mmWave Studio&lt;/strong&gt;：射频评估工具，配合 DCA1000EVM 采原始 ADC 数据用的。单纯跑 Demo 不需要这个&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;SDK 和工具都能在 TI 官网免费下载，不过得填出口合规审批表，等个一分钟左右就能拿到下载链接。&lt;/p&gt;
&lt;h2&gt;上手体验&lt;/h2&gt;
&lt;p&gt;不写一行代码，插上板子就能看雷达效果，这是 IWR1642BOOST 最爽的地方。TI 在 SDK 里预编译了一个 out-of-box demo，DSP 上跑了完整的 FMCW 处理链（距离维 FFT → 多普勒维 FFT → CFAR 检测 → 角度估计），R4F 负责把点云通过 UART 吐出来。&lt;/p&gt;
&lt;h3&gt;第一步：烧录固件&lt;/h3&gt;
&lt;p&gt;用 UniFlash 往板子烧 SDK 里的 &lt;code&gt;xwr16xx_mmw_demo.bin&lt;/code&gt;。操作很简单：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;USB 线连接板子的 XDS110 口（板上标了 &lt;code&gt;USB&lt;/code&gt; 的那个 Micro USB 口，不是标 &lt;code&gt;CAN&lt;/code&gt; 的那个）&lt;/li&gt;
&lt;li&gt;确认 SOP 跳线帽在模式 1（SOP0 短接，SOP1 和 SOP2 断开）——这是 Flash 编程模式&lt;/li&gt;
&lt;li&gt;打开 UniFlash，选 IWR1642 芯片，自动检测到 XDS110 调试器&lt;/li&gt;
&lt;li&gt;在 Program 页面选 &lt;code&gt;xwr16xx_mmw_demo.bin&lt;/code&gt;，点 Load Image 开始烧录&lt;/li&gt;
&lt;li&gt;烧完后断电，把 SOP 跳线帽改回模式 0（SOP0 断开），重新上电&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;SOP（Sense on Power）是 IWR1642 的启动模式选择。SOP0=0 表示从 Flash 启动运行 App；SOP0=1 进入编程模式等待刷固件；SOP0=SOP1=1 则是硬件看门狗调试模式。日常跑 Demo 保持在 SOP0 断开就行。&lt;/p&gt;
&lt;p&gt;板子上的 DS3 LED（Nerr LED）在正常运行时应该是熄灭的。程序跑起来后 DS1 会规律闪烁表示心跳，说明固件启动成功。&lt;/p&gt;
&lt;h3&gt;第二步：mmWave Demo Visualizer&lt;/h3&gt;
&lt;p&gt;固件跑起来之后，打开 &lt;a href=&quot;https://dev.ti.com/gallery/view/mmwave/mmWave_Demo_Visualizer/ver/3.6.0/&quot;&gt;mmWave Demo Visualizer&lt;/a&gt;，浏览器需要支持 Web Serial API（Chrome/Edge 都行）。&lt;/p&gt;
&lt;p&gt;连接流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;页面上先选硬件平台为 xWR1642，然后点 Setup&lt;/li&gt;
&lt;li&gt;在 Serial Ports 里选板子的两个串口——XDS110 一般映射为两个 COM 口，一个标 Application/User UART（数据口），一个标 Auxiliary Data Port（配置口）。不确定哪个是哪个的话先选 Configuration Port，波特率 115200，点 Connect&lt;/li&gt;
&lt;li&gt;连接成功后端口状态会变绿，这时候可以 Load Config 加载 SDK 里自带的配置文件（比如 &lt;code&gt;profile_2d.cfg&lt;/code&gt;），或者直接用页面默认的参数&lt;/li&gt;
&lt;li&gt;点击 Send Config to Device，雷达开始工作&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;然后就能在 Plots 页面看到效果了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;3D 点云&lt;/strong&gt;：实时刷新，每个点代表检测到的目标，颜色深浅表示反射强度（SNR）。人在雷达前来回走动，点云跟着移动，相当直观&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;距离多普勒热力图&lt;/strong&gt;：横轴是速度（靠近为正远离为负），纵轴是距离，亮度表示回波强度。静止物体集中在零速线上，运动目标的位置一目了然&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;距离剖面&lt;/strong&gt;：一维距离像，能看出不同距离上的反射能量分布，用来调 CFAR 门限时很好用&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;统计信息&lt;/strong&gt;：帧率、检测到的目标数量、CPU 负载等&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;默认参数是 2D 检测，只给水平面的 X/Y 坐标。如果加载 &lt;code&gt;profile_3d.cfg&lt;/code&gt;，能看到 Z 轴（高度）信息。不过 3D 模式下角度分辨率有限，主要是靠俯仰天线阵列的布局来区分地面和人体反射。&lt;/p&gt;
&lt;h3&gt;几个实用的参数调整&lt;/h3&gt;
&lt;p&gt;Visualizer 左上角的参数面板可以直接改配置并实时下发，不用重新烧固件。几个常调的参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Range Resolution&lt;/strong&gt;：距离分辨率，越小能区分越近的目标，但最大探测距离会缩短。室内人员检测一般 4-5cm 够用了&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Maximum Range&lt;/strong&gt;：最大探测距离，室内场景设 5-10 米足够&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Frame Periodicity&lt;/strong&gt;：帧周期，默认 50ms（20fps），调到 100ms 能省不少功耗&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CFAR Threshold&lt;/strong&gt;：恒虚警检测门限，调低能拿到更多弱反射目标但误检会增加，室内人员检测可以稍微激进一点&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果板子发热比较厉害（IWR1642 满载时功耗能到 2W+），可以考虑把帧周期拉长一点，DSP 间歇性干活温度会下来不少。&lt;/p&gt;
&lt;h2&gt;能拿来做什么&lt;/h2&gt;
&lt;p&gt;IWR1642BOOST 配套了不少参考设计，都是可以直接跑的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;人员计数与跟踪&lt;/strong&gt;：用在会议室、商场统计人流。DSP 上跑了 group tracking 算法，跟踪帧间同一个目标&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;交通监控&lt;/strong&gt;：检测 80 米内的车辆，同时区分轿车和卡车。停车场和路口场景验证过&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生命体征检测&lt;/strong&gt;：通过胸腔微小的位移变化提取呼吸和心率，信号极弱但对 DSP 的算法要求很高&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;手势识别&lt;/strong&gt;：利用多普勒特征区分不同的手势方向，适合非接触交互场景&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;毫米波跟摄像头比有个优势就是不涉及隐私，输出的就是点云坐标，没有图像。所以安防和智能家居场景里比视觉方案更容易过合规审查。缺点是点云稀疏，多目标遮挡时容易丢跟踪，静态目标感知也比较弱——一个人站着不动时间长了可能会被 CFAR 滤掉。&lt;/p&gt;</content:encoded></item><item><title>HackRF One 与 PortaPack H4</title><link>https://saurlax.com/blog/portapack-h4/</link><guid isPermaLink="true">https://saurlax.com/blog/portapack-h4/</guid><description>去年写过一篇用 [GNURadio 搭 HackRF One 做广播接收](/blog/gnuradio/)的折腾记录，当时插着 USB 连电脑玩了大半年，收 FM 广播、看频谱。最近手痒入了一块 P...</description><pubDate>Sun, 28 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;去年写过一篇用 &lt;a href=&quot;/blog/gnuradio/&quot;&gt;GNURadio 搭 HackRF One 做广播接收&lt;/a&gt;的折腾记录，当时插着 USB 连电脑玩了大半年，收 FM 广播、看频谱。最近手痒入了一块 PortaPack H4，插上之后 HackRF 瞬间从 PC 外设变成了一台独立手持设备——不用电脑、不用 GNURadio 拖流程图，开机就能用。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;portapack-h4-board.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;PortaPack 本质上是 HackRF One 的扩展背板，自带屏幕、电池、按键和 microSD 卡槽，通过排针直接插在 HackRF 上面。装上之后 HackRF 就不再依赖电脑做信号处理了——PortaPack 内置了一颗 FPGA 配合固件完成基带处理，数据直接存卡或显示在屏幕上。&lt;/p&gt;
&lt;p&gt;市面上的 PortaPack 有几个版本：H1 是最早的，屏幕小、没有触摸；H2 和 H2+ 在 H1 基础上加了锂电池充电和触摸屏；H4 则是目前比较新的款式，屏幕更大（3.2 寸），电容触摸，电池管理用了 MAX17055 电量计芯片，看剩余电量比 H2 系列准不少。H4M 是 H4 的一个小改款，区别在于屏幕排线和部分元件布局，固件通用。&lt;/p&gt;
&lt;h2&gt;烧录固件&lt;/h2&gt;
&lt;p&gt;HackRF 原厂固件只支持 USB 连接电脑做 SDR 前端，插入 PortaPack 之后需要刷第三方固件才能让屏幕和按键工作。目前社区主流的固件是 Mayhem，它是 Havoc 固件的一个分支，由一群无线电爱好者在维护，更新非常活跃。&lt;/p&gt;
&lt;p&gt;Mayhem 的 GitHub 仓库在 &lt;a href=&quot;https://github.com/portapack-mayhem/mayhem-firmware&quot;&gt;portapack-mayhem/mayhem-firmware&lt;/a&gt;，Releases 页面有稳定版和每日构建版。稳定版功能完整、Bug 少，适合日常用；nightly 则尝尝鲜，偶尔会有些还在调试中的新功能。&lt;/p&gt;
&lt;p&gt;烧录比想象中简单很多，Mayhem 提供了两种几乎零门槛的方式，既不用装驱动也不用敲命令。&lt;/p&gt;
&lt;h3&gt;方式一：hackrf.app&lt;/h3&gt;
&lt;p&gt;Mayhem 官方提供了一个 Web 刷写工具 &lt;a href=&quot;https://hackrf.app/&quot;&gt;hackrf.app&lt;/a&gt;，利用浏览器的 WebUSB API 直接跟设备通信，不用装任何驱动或工具——打开网页、插上线、点几下鼠标就完事。&lt;/p&gt;
&lt;p&gt;条件是 PortaPack 已经刷过 Mayhem v2.0.1 以上版本，且用 Chrome 或 Edge 这类支持 WebUSB 的浏览器。&lt;/p&gt;
&lt;p&gt;操作流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用数据线把 PortaPack 连到电脑（注意必须是数据线，只充电的线不行），设备保持 Mayhem 正常运行状态，不需要切什么特殊模式&lt;/li&gt;
&lt;li&gt;浏览器打开 &lt;a href=&quot;https://hackrf.app/&quot;&gt;hackrf.app&lt;/a&gt;，点页面上的 Connect Device&lt;/li&gt;
&lt;li&gt;弹出的设备列表里选 HackRF 点连接，页面顶部会显示 HackRF Connected 并实时截取设备屏幕画面&lt;/li&gt;
&lt;li&gt;往下翻找到 Manage Firmware，选 Stable（稳定版）或 Nightly。也可以上传自己下载的 &lt;code&gt;.ppfw&lt;/code&gt; 文件走 Custom 通道&lt;/li&gt;
&lt;li&gt;确认后等待上传写入，设备会自动重启完成更新&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;整个过程不用拔 SD 卡、不用找固件文件放哪个目录、更不用记什么按键组合。页面还能实时看设备屏幕，刷完当场验证。推荐选 Stable 通道，省心。&lt;/p&gt;
&lt;p&gt;刷完记得同步更新 SD 卡内容——Releases 里下对应的 &lt;code&gt;mayhem_vX.Y.Z_COPY_TO_SDCARD.7z&lt;/code&gt;，解压覆盖到 SD 卡根目录，保证外置应用和固件版本匹配。&lt;/p&gt;
&lt;h3&gt;方式二：脚本&lt;/h3&gt;
&lt;p&gt;如果不想开浏览器，GitHub Releases 页面也提供了 Windows 下的 &lt;code&gt;mayham_flasher.bat&lt;/code&gt; 脚本。下载固件压缩包解压后，双击 &lt;code&gt;mayham_flasher.bat&lt;/code&gt; 就能自动完成烧录，适合批量操作或者习惯本地工具的人。&lt;/p&gt;
&lt;p&gt;两种方式本质上都走的是 Mayhem 固件内置的 USB 串口升级通道，比传统 DFU 模式要按住按钮插 USB 再装 Zadig 驱动的硬核流程方便太多。除非刷黑屏变砖需要救急，一般用不到 DFU 那套。&lt;/p&gt;
&lt;h2&gt;常用功能&lt;/h2&gt;
&lt;p&gt;Mayhem 固件内置了几十个应用，从接收、发射到各种实用工具，甚至塞了几个经典游戏进去。&lt;/p&gt;
&lt;h3&gt;接收类&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;ADS-B 航空追踪&lt;/strong&gt;：ADS-B 是民航客机广播自身位置、高度、速度的协议，频率在 1090MHz。PortaPack 上打开 ADS-B 应用就能看到附近飞机的呼号、经纬度、高度和航向，配合瀑布图还能看到信号脉冲。数据可以实时显示，也能存 SD 卡导出回 GIS 软件画航线图。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;POCSAG 寻呼机解码&lt;/strong&gt;：有些内部通信还在用寻呼机，POCSAG 协议完全不加密，PortaPack 收到后直接显示文字内容。频率一般在 150MHz 和 450MHz 附近，用 Scanner 扫一圈很容易找到。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AF 音频接收&lt;/strong&gt;：最基础也最常用的功能，相当于一台全频段收音机。覆盖 HackRF 能收到的所有频率（1MHz-6GHz），支持 AM、NFM、WFM 解调。收听本地 FM 广播、机场塔台通话、业余无线电中继台都没问题。触摸屏可以直接拖动频谱选台，比传统旋钮方便。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NOAA 气象卫星&lt;/strong&gt;：接上合适的 V 型双极天线（137MHz），等 NOAA 卫星过顶的时候能收到云图。PortaPack 上 NOAA 应用可以自动捕获 APT 信号并解码成黑白图像，不需要电脑端后续处理。不过效果受天线影响很大，室内基本没戏，得上天台或者开阔地。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SSTV 慢扫描电视&lt;/strong&gt;：业余无线电爱好者常玩的东西，把图片编码成音频信号传输。国际空间站（ISS）偶尔会 SSTV 下发纪念图片，频率 145.800MHz，用 PortaPack 配合一根合适的天线就能收到。&lt;/p&gt;
&lt;h3&gt;发射类&lt;/h3&gt;
&lt;p&gt;Mayhem 固件支持信号发射，但 HackRF 是个未做功率放大的裸 SDR 前端，输出功率极低，在不接功放的情况下几乎没有实用通信距离。发射类应用更多是实验性质——比如 Signal Gen 生成一个正弦波拿来校准其他接收设备，或者用 OOK 编码控制一些简单的 433MHz 遥控插座。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Spectrum Painter&lt;/strong&gt;：可以把自己画的图案编码成 OFDM 信号发射出去，在频谱仪上能看到图形。&lt;/p&gt;
&lt;p&gt;注意：发射务必确认频率在合法范围内，不要干扰正常通信。HAM 频段在持有操作证的情况下可合法发射。&lt;/p&gt;
&lt;h3&gt;实用工具&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Capture / Replay&lt;/strong&gt;：录制一段 IQ 采样到 SD 卡，然后原样回放出来。这在调试遥控器（比如 315/433MHz 的汽车钥匙、卷帘门遥控）时很有用：先 Capture 录下按键时的信号，再用 Replay 重放测试。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Recon 频谱监测&lt;/strong&gt;：扫描一段频率范围，把所有活跃信号的时间和频率记录到文件里。适合监控某频段的活动规律，比如想知道某个对讲频段什么时候最热闹，挂一晚 Recon 就可以看出来了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Freq Manager 频率管理&lt;/strong&gt;：把常用的频率存成列表，各个接收应用里统一调用。不用每次都手动输频率。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;portapack-h4.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;</content:encoded></item><item><title>钛比科技 NB-IoT 开发板</title><link>https://saurlax.com/blog/terabits-nbiot-devboard/</link><guid isPermaLink="true">https://saurlax.com/blog/terabits-nbiot-devboard/</guid><description>前阵子从朋友那薅来一块钛比科技的 NB-IoT 开发板，STM32F103ZET + 移远 BC95 模组架构。

![](terabits-nbiot-devboard.jpg)

## 什么是 N...</description><pubDate>Sun, 28 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;前阵子从朋友那薅来一块钛比科技的 NB-IoT 开发板，STM32F103ZET + 移远 BC95 模组架构。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;terabits-nbiot-devboard.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;什么是 NB-IoT&lt;/h2&gt;
&lt;p&gt;NB-IoT（Narrowband IoT）是 3GPP 在 Release 13 定义的一种低功耗广域网技术，直接跑在运营商授权频段上。跟 LoRa、Sigfox 这些非授权频段的方案相比，最大的优势是不用自己搭基站，插张物联网卡就能用，覆盖也比 2G/4G 更深——号称能穿两堵墙还有信号。&lt;/p&gt;
&lt;p&gt;PSM 和 eDRX 两种省电模式是它跟传统蜂窝通信拉开差距的地方。PSM 模式下模组深度休眠，下行不可达但功耗能压到微安级；eDRX 则是在两次寻呼之间拉长间隔，兼顾省电和实时性。对电池供电的物联网终端来说，两个模式基本是标配。&lt;/p&gt;
&lt;p&gt;华为和中国电信在这块的推动力度很大，国内 NB-IoT 网络覆盖已经比较成熟了。简单理解的话，它就是为水表、路灯、烟感这类低频小数据量场景量身定制的蜂窝通信方案。&lt;/p&gt;
&lt;h2&gt;硬件概览&lt;/h2&gt;
&lt;p&gt;板子核心是两颗芯片：STM32F103ZET6 做应用处理器，移远 BC95-B5 做 NB-IoT 通信模组。F103ZET6 是 Cortex-M3 内核，72MHz 主频，128KB Flash、20KB RAM，112 个 GPIO——在 F1 系列里算顶配了，串口、I2C、SPI、12 位 ADC 一应俱全。&lt;/p&gt;
&lt;p&gt;BC95 模组通过 UART 跟主控通信，AT 指令集驱动。模组独立供电 3.8V，峰值电流能到 2A——NB-IoT 发射瞬间的电流尖峰确实不小，板子专门给模组做了单独一路 LDO 保证瞬态响应，这个细节处理得不错。&lt;/p&gt;
&lt;p&gt;板载资源列一下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;8 路拨码开关，拿来配硬件参数或切换启动模式&lt;/li&gt;
&lt;li&gt;3 个 LED、2 个复位按键&lt;/li&gt;
&lt;li&gt;JTAG/SWD 调试口，接 ST-Link 就能在线调试&lt;/li&gt;
&lt;li&gt;RS232 和 RS485 接口，工业场景能用上&lt;/li&gt;
&lt;li&gt;SMA 天线接口，50Ω 阻抗匹配，接收灵敏度 -135dBm&lt;/li&gt;
&lt;li&gt;12V DC 供电，板上 LDO 分别输出 3.3V 和 3.8V&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;扩展方面，I2C 可以挂 SHT30 温湿度传感器，SPI 能接 LCD 或 LoRa 模块，可玩性还行。&lt;/p&gt;
&lt;h2&gt;开发环境&lt;/h2&gt;
&lt;p&gt;以前搞 STM32 基本绕不开 Keil，界面古老不说，编辑器体验属实折磨人。我之前写过一篇&lt;a href=&quot;/blog/stm32cubeide-vscode/&quot;&gt;用 STM32CubeIDE for VSCode 开发的记录&lt;/a&gt;，这次继续这套流程。&lt;/p&gt;
&lt;p&gt;简单说就是：STM32CubeMX 生成 CMake 工程（Toolchain 选 GCC），VSCode 装 STM32CubeIDE 插件包，打开项目目录自动识别，点一下转换就能编译调试。不用装 CubeIDE 本体，插件包自带了编译器和调试器，省掉好几个 GB 的 IDE 安装。&lt;/p&gt;
&lt;p&gt;大致步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;VSCode 扩展市场搜 &lt;code&gt;STM32CubeIDE for VSCode&lt;/code&gt; 安装，左侧栏会多一个 STM32 图标&lt;/li&gt;
&lt;li&gt;CubeMX 里选 STM32F103ZET6，配好引脚和时钟，Project Manager 里 Toolchain/IDE 选 CMake&lt;/li&gt;
&lt;li&gt;Generate Code 后在 VSCode 打开项目文件夹，插件自动检测到 CMake 项目&lt;/li&gt;
&lt;li&gt;左下角齿轮图标编译，运行和调试里选 ST-Link GDB Server 就能烧录调试&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;引脚配置不复杂：USART1（PA9/PA10）接 BC95 模组做 AT 指令通道，USART2（PA2/PA3）留作 printf 调试输出，SWD（PA13/PA14）接 ST-Link，再配一个 LED（PE5）做状态指示。&lt;/p&gt;
&lt;p&gt;CubeMX 生成代码后，用 &lt;code&gt;HAL_UARTEx_ReceiveToIdle_DMA&lt;/code&gt; 来收 BC95 的应答帧——这个 API 是 HAL 库比较新的接口，利用串口空闲中断自动检测帧尾，比老式的定长接收灵活太多。&lt;/p&gt;
&lt;h2&gt;AT 指令驱动 BC95 上云&lt;/h2&gt;
&lt;p&gt;玩 NB-IoT 绕不开的一件事就是用 AT 指令把模组怼上网，然后发数据到平台。我这次做的 Demo 不复杂，上电后 LED 慢闪表示正在注册网络，注册成功后 LED 常亮，每分钟通过 UDP 往服务器发一条模拟的温度上报。&lt;/p&gt;
&lt;h3&gt;AT 指令封装&lt;/h3&gt;
&lt;p&gt;BC95 的 AT 交互需要一套带超时的发送-应答机制。先发指令，然后在串口中断里攒应答数据，主循环轮询匹配关键字：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;typedef enum {
  BC95_OK,
  BC95_TIMEOUT,
  BC95_ERROR
} BC95_Status;

#define BC95_RX_BUF_SIZE 512
static char bc95_rx_buf[BC95_RX_BUF_SIZE];
static volatile uint8_t bc95_rx_ready = 0;

BC95_Status BC95_SendCmd(const char *cmd, const char *expect,
                         uint32_t timeout_ms) {
  bc95_rx_ready = 0;
  memset(bc95_rx_buf, 0, BC95_RX_BUF_SIZE);

  HAL_UART_Transmit(&amp;amp;huart1, (uint8_t *)cmd, strlen(cmd), 100);
  HAL_UART_Transmit(&amp;amp;huart1, (uint8_t *)&amp;quot;\r\n&amp;quot;, 2, 100);

  uint32_t tick = HAL_GetTick();
  while (HAL_GetTick() - tick &amp;lt; timeout_ms) {
    if (bc95_rx_ready) {
      bc95_rx_ready = 0;
      if (strstr(bc95_rx_buf, expect))
        return BC95_OK;
      if (strstr(bc95_rx_buf, &amp;quot;ERROR&amp;quot;))
        return BC95_ERROR;
    }
    HAL_Delay(10);
  }
  return BC95_TIMEOUT;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;串口接收回调用 Idle 中断触发，DMA 循环接收：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) {
  if (huart-&amp;gt;Instance == USART1) {
    bc95_rx_ready = 1;
    bc95_rx_buf[Size] = &apos;\0&apos;;
    HAL_UARTEx_ReceiveToIdle_DMA(&amp;amp;huart1, (uint8_t *)bc95_rx_buf,
                                  BC95_RX_BUF_SIZE);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;网络注册&lt;/h3&gt;
&lt;p&gt;BC95 上电后走一套标准注册流程。先检查 SIM 卡状态，再查信号和注册状态：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int BC95_Init(void) {
  // 测试通信
  if (BC95_SendCmd(&amp;quot;AT&amp;quot;, &amp;quot;OK&amp;quot;, 2000) != BC95_OK) return -1;

  // 关闭回显，减少数据解析的噪音
  BC95_SendCmd(&amp;quot;ATE0&amp;quot;, &amp;quot;OK&amp;quot;, 1000);

  // 查询 SIM 卡状态，READY 表示正常
  if (BC95_SendCmd(&amp;quot;AT+CPIN?&amp;quot;, &amp;quot;+CPIN: READY&amp;quot;, 2000) != BC95_OK) {
    printf(&amp;quot;SIM error\r\n&amp;quot;);
    return -1;
  }

  // 查询信号质量
  BC95_SendCmd(&amp;quot;AT+CSQ&amp;quot;, &amp;quot;OK&amp;quot;, 2000);

  // 查询网络注册状态，1=本地注册 5=漫游注册
  if (BC95_SendCmd(&amp;quot;AT+CEREG?&amp;quot;, &amp;quot;+CEREG: 0,1&amp;quot;, 5000) != BC95_OK &amp;amp;&amp;amp;
      BC95_SendCmd(&amp;quot;AT+CEREG?&amp;quot;, &amp;quot;+CEREG: 0,5&amp;quot;, 5000) != BC95_OK) {
    printf(&amp;quot;Network registration failed\r\n&amp;quot;);
    return -1;
  }

  // 获取模组 IP，确认附着成功
  BC95_SendCmd(&amp;quot;AT+CGPADDR&amp;quot;, &amp;quot;OK&amp;quot;, 2000);

  printf(&amp;quot;NB-IoT network ready\r\n&amp;quot;);
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有个坑值得提一下：&lt;code&gt;AT+CSQ&lt;/code&gt; 返回的 &lt;code&gt;+CSQ: 99,99&lt;/code&gt; 不一定表示没信号，BC95 在 PSM 唤醒后首次查信号经常返回 99，实际上网络是通的。判断联网与否还是以 &lt;code&gt;AT+CEREG?&lt;/code&gt; 的注册状态和 &lt;code&gt;AT+CGPADDR&lt;/code&gt; 获取到的 IP 为准。&lt;/p&gt;
&lt;h3&gt;UDP 数据发送&lt;/h3&gt;
&lt;p&gt;BC95 走 UDP 用 socket 方式操作，创建 socket、发包、关闭一套带走：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int BC95_SendUDP(const char *ip, uint16_t port,
                 const uint8_t *data, uint16_t len) {
  char cmd[256];
  char hex[2];

  // 创建 UDP socket，protocol=17 表示 UDP
  if (BC95_SendCmd(&amp;quot;AT+NSOCR=DGRAM,17,0,1&amp;quot;, &amp;quot;OK&amp;quot;, 3000) != BC95_OK)
    return -1;

  // 拼装 AT+NSOST 指令，数据部分要转十六进制字符串
  int pos = snprintf(cmd, sizeof(cmd), &amp;quot;AT+NSOST=0,%s,%u,%u,&amp;quot;,
                     ip, port, len);
  for (int i = 0; i &amp;lt; len; i++) {
    snprintf(hex, sizeof(hex), &amp;quot;%02X&amp;quot;, data[i]);
    cmd[pos++] = hex[0];
    cmd[pos++] = hex[1];
  }
  cmd[pos] = &apos;\0&apos;;

  if (BC95_SendCmd(cmd, &amp;quot;OK&amp;quot;, 5000) != BC95_OK) {
    BC95_SendCmd(&amp;quot;AT+NSOCL=0&amp;quot;, &amp;quot;OK&amp;quot;, 2000);
    return -1;
  }

  // 关闭 socket 释放资源
  BC95_SendCmd(&amp;quot;AT+NSOCL=0&amp;quot;, &amp;quot;OK&amp;quot;, 2000);
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;AT+NSOCR&lt;/code&gt; 的几个参数需要留意：&lt;code&gt;DGRAM&lt;/code&gt; 表示数据报类型，&lt;code&gt;17&lt;/code&gt; 是 UDP 协议号，&lt;code&gt;0&lt;/code&gt; 是本地端口（0 表示自动分配），&lt;code&gt;1&lt;/code&gt; 是接收控制（不接收）。&lt;/p&gt;
&lt;h3&gt;主循环：定时上报&lt;/h3&gt;
&lt;p&gt;主循环里每秒自增计数器，满 60 秒就发一条 JSON 格式的温度数据：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int main(void) {
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_USART1_UART_Init();
  MX_USART2_UART_Init();

  // 启动 DMA 接收
  HAL_UARTEx_ReceiveToIdle_DMA(&amp;amp;huart1, (uint8_t *)bc95_rx_buf,
                                BC95_RX_BUF_SIZE);

  // LED 闪烁等待网络注册
  while (BC95_Init() != 0) {
    HAL_GPIO_TogglePin(GPIOE, GPIO_PIN_5);
    HAL_Delay(500);
    printf(&amp;quot;Retrying network registration...\r\n&amp;quot;);
  }

  // 常亮表示就绪
  HAL_GPIO_WritePin(GPIOE, GPIO_PIN_5, GPIO_PIN_SET);

  uint8_t counter = 0;
  while (1) {
    HAL_Delay(1000);
    counter++;

    if (counter &amp;gt;= 60) {
      counter = 0;
      float temp = 25.0f + (float)(HAL_GetTick() % 1000) / 100.0f;

      uint8_t payload[32];
      int len = snprintf((char *)payload, sizeof(payload),
                         &amp;quot;{\&amp;quot;temp\&amp;quot;:%.1f}&amp;quot;, temp);

      printf(&amp;quot;Sending: %s\r\n&amp;quot;, payload);
      if (BC95_SendUDP(&amp;quot;xxx.xxx.xxx.xxx&amp;quot;, 8888, payload, len) == BC95_OK) {
        printf(&amp;quot;Send OK\r\n&amp;quot;);
      } else {
        printf(&amp;quot;Send failed\r\n&amp;quot;);
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;服务端用 netcat 开个 UDP 监听就能收到数据：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;nc -u -l 8888
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果想接入正式平台，BC95 也支持 CoAP（&lt;code&gt;AT+NCOAP&lt;/code&gt; 系列指令），可以直接怼华为 OceanConnect 或者电信 IoT 平台，注册设备后拿到 endpoint 和 token 就行。MQTT 的话 BC95 同样有 &lt;code&gt;AT+QMTOPEN&lt;/code&gt;/&lt;code&gt;AT+QMTCONN&lt;/code&gt; 这套指令，走阿里云 IoT、OneNET 都没问题，流程大同小异。&lt;/p&gt;</content:encoded></item><item><title>Sätteri 与 unified：Markdown 处理的两条路线</title><link>https://saurlax.com/blog/satteri-and-unified/</link><guid isPermaLink="true">https://saurlax.com/blog/satteri-and-unified/</guid><description>前几天看到一个消息，Astro 7 正式发布，默认换上了 Rust 写的 Sätteri 作为 Markdown 处理器。构建速度更快，插件更轻量，听起来一切都很美好。我的站点正好用 Astro，就顺...</description><pubDate>Tue, 23 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;前几天看到一个消息，Astro 7 正式发布，默认换上了 Rust 写的 Sätteri 作为 Markdown 处理器。构建速度更快，插件更轻量，听起来一切都很美好。我的站点正好用 Astro，就顺手升级了。&lt;/p&gt;
&lt;p&gt;结果 build 直接报错。原来是 Sätteri 不再内置 unified 的那套流水线，而我在 &lt;code&gt;astro.config.ts&lt;/code&gt; 里配的 remark-math 和 rehype-katex 都是基于 unified 生态的插件，现在不兼容了。&lt;/p&gt;
&lt;p&gt;我一开始还以为是配置问题，花了不少时间查文档。后来发现，Sätteri 的 AST 跟 MDAST 完全不是一回事，没有现成的 math 插件可以用。想继续用数学公式，要么自己写一个 Sätteri 插件，要么装回 &lt;code&gt;@astrojs/markdown-remark&lt;/code&gt; 继续走 unified 的老路。&lt;/p&gt;
&lt;h2&gt;unified 的流水线&lt;/h2&gt;
&lt;p&gt;先说说 unified 是怎么工作的。Markdown 文本先进 remark-parse，出来的是一棵 MDAST 语法树。然后 remark 插件在这棵树上做变换，接着 remark-rehype 把树转成 HAST，rehype 插件继续改，最后 stringify 成 HTML。&lt;/p&gt;
&lt;p&gt;这条流水线能跑通，全靠规范。MDAST 规定了 Markdown 节点长什么样，HAST 规定了 HTML 节点长什么样，unist 又给了共同的底层接口。三层规范叠在一起，插件之间才能互相协作。&lt;/p&gt;
&lt;p&gt;remark-math 在 MDAST 阶段把 &lt;code&gt;$...$&lt;/code&gt; 识别成 math 节点，rehype-katex 在 HAST 阶段把这些节点转成 KaTeX 渲染后的 HTML。两个插件并不直接通信，但通过共享的 AST 规范间接配合。这种生态的丰富程度，是经过十年积累才有的——几百个插件，几乎你想得到的需求都有人做过。&lt;/p&gt;
&lt;p&gt;但层数多了，AST 要被反复遍历。规范卡得太死，想做的事情如果不符合节点定义，就得绕路或者自定义类型。代价确实存在，只是以前没人太在意，因为生态太舒服了。&lt;/p&gt;
&lt;h2&gt;Sätteri 的扁平化&lt;/h2&gt;
&lt;p&gt;Sätteri 的做法完全不同。解析完 Markdown 直接得到一棵树，插件拿到树随便改。没有 MDAST，没有 HAST，没有 remark 和 rehype 的边界。&lt;/p&gt;
&lt;p&gt;Rust 本身比 JavaScript 快一个数量级，省掉中间层的多次转换后，构建时间确实能缩不少。插件写起来也更直接，不需要 visitor 模式，拿到树，找到节点，改了就行。&lt;/p&gt;
&lt;p&gt;我在 Astro 7 里第一次体验这种架构时，构建速度的提升是能感觉到的。但当我试图把 remark-math 和 rehype-katex 加回去的时候，问题就来了——Sätteri 不认识这些插件，因为它们操作的是 MDAST 和 HAST，不是 Sätteri 自己的 AST。&lt;/p&gt;
&lt;p&gt;想继续用数学公式，理论上可以写一个 Sätteri 的 math 插件。但 Sätteri 的 AST 是它自己定的，没有社区共识。你写的插件，别的插件可能不认识。unified 里那种插件链上各自处理、最终拼出完整页面的模式，在这里很难复现。&lt;/p&gt;
&lt;p&gt;也就是说，我不是在换一个更快的处理器，而是在换一个全新的、几乎没有插件的生态。这跟当年从 Webpack 切到 Vite 还不一样，Vite 至少兼容 Rollup 插件。Sätteri 这边，你基本得从零开始。&lt;/p&gt;
&lt;h2&gt;那我现在怎么办？&lt;/h2&gt;
&lt;p&gt;降级回 v6。&lt;/p&gt;</content:encoded></item><item><title>M5Stack Cardputer ADV 上手</title><link>https://saurlax.com/blog/m5stack-cardputer-adv/</link><guid isPermaLink="true">https://saurlax.com/blog/m5stack-cardputer-adv/</guid><description>![](./cardputer.jpg)

最近刷到 [M5Stack](https://m5stack.com/) 的 [Cardputer ADV](https://docs.m5stack.co...</description><pubDate>Sat, 20 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;./cardputer.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最近刷到 &lt;a href=&quot;https://m5stack.com/&quot;&gt;M5Stack&lt;/a&gt; 的 &lt;a href=&quot;https://docs.m5stack.com/en/core/Cardputer%20ADV&quot;&gt;Cardputer ADV&lt;/a&gt;，信用卡大小的机身塞了块键盘，觉得挺有意思，顺手就入了一台。&lt;/p&gt;
&lt;h2&gt;M5Stack 的产品线&lt;/h2&gt;
&lt;p&gt;M5Stack 这家公司做模块化开发板起家，核心思路是把 ESP32 包成一个带屏幕、带外壳、带接口的成品。早期出的是 Core 系列，像个小型游戏机，堆叠各种功能模块。后面又做了 Stick、Atom、Paper 各种形态，Cardputer 算是他们把键盘和屏幕整合到一块的尝试。&lt;/p&gt;
&lt;p&gt;Cardputer 最大的特点是自带全键盘，56 键紧凑布局，导航键在右侧。比起其他 M5 产品靠几个按钮或者触摸屏输入，有实体键盘在写命令、输密码、调参数的时候顺手太多。&lt;/p&gt;
&lt;p&gt;这台 ADV 版底部带 CAP（Communication Accessory Port）接口，可以插拓展模块。官方目前出了好几款 CAP 配件，我用的是 LoRa+GNSS 通信模块。模块上同时集成了 SX1262 射频芯片和 GNSS 定位芯片，LoRa 部分支持 433MHz、868MHz 和 915MHz 三个频段，国内用 433MHz 居多。&lt;/p&gt;
&lt;p&gt;插上去之后，Cardputer ADV 就变成了一个带键盘的手持 LoRa 电台。在户外测过，市区环境里两公里内能稳定通信，开阔地带据说能到五公里以上。配合 Cardputer 的全键盘，可以手写发送文本消息，比那些只有几个按钮的 LoRa 节点方便太多。&lt;/p&gt;
&lt;p&gt;更实用的是 GNSS 功能。模块上的定位芯片能直接输出经纬度坐标，在屏幕上实时显示当前位置，或者把位置信息打包进 LoRa 数据包发出去。对于野外活动或者需要位置上报的场景，一台设备同时解决通信和定位两个问题，不用再外接 GPS 模块。&lt;/p&gt;
&lt;p&gt;模块通过 SPI 跟主控通信，M5 的 Arduino 库已经封装好了相关 API，LoRa 初始化和 GNSS 数据读取都有现成的函数，不需要自己处理射频底层或者 NMEA 协议解析。&lt;/p&gt;
&lt;h2&gt;UIFlow 和 M5Burner&lt;/h2&gt;
&lt;p&gt;M5Stack 在软件生态上花了挺大功夫，降低了入门门槛。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://uiflow2.m5stack.com/&quot;&gt;UIFlow&lt;/a&gt; 是他们的图形化编程平台，基于 Blockly 拼图块，在浏览器里拖拖拽拽就能写程序。对于没接触过嵌入式的人来说，不用配环境、不用学 C++，连上设备就能跑。它同时支持 MicroPython，拼图块生成的代码可以一键切换成脚本，适合从可视化过渡到代码写法的场景。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.m5stack.com/en/download&quot;&gt;M5Burner&lt;/a&gt; 是固件烧录工具，支持 Windows、macOS 和 Linux。它最大的价值在于内置了一个固件市场，罗列了大量社区和官方预编译好的固件。选中设备型号，挑一个固件，点下载再点烧录，几步搞定。Cardputer ADV 的驱动和分区表也内置好了，不用像普通 ESP32 那样手动配置。&lt;/p&gt;
&lt;h2&gt;社区固件&lt;/h2&gt;
&lt;p&gt;Cardputer 的键盘和屏幕组合吸引了不少人做工具类固件，其中有两个项目特别出名。&lt;/p&gt;
&lt;p&gt;Evil M5 是一个 WiFi 安全测试工具，功能涵盖了 WiFi 扫描、伪造热点、Deauth 攻击、Probe 请求嗅探等。把它理解成一个掌上版的 WiFi Pineapple，带屏幕和键盘，操作起来比纯命令行直观很多。当然，这个项目仅供学习和测试自有网络，实际使用要注意法律边界。&lt;/p&gt;
&lt;p&gt;M5 Launcher 则是一个固件启动器，解决的是多固件切换的痛点。Cardputer 存储空间不小，但每次刷不同固件都要重新烧录挺麻烦。M5 Launcher 把设备变成一个桌面系统，各个应用和固件以图标形式排列，像手机桌面一样直接点选启动，不用反复插线刷机。&lt;/p&gt;
&lt;h2&gt;简单尝试一下&lt;/h2&gt;
&lt;p&gt;说了这么多，最后拿我自己写的一个小玩意当例子，很简单，就是在屏幕上循环播放月薪猫的跳散味舞。&lt;/p&gt;
&lt;p&gt;https://www.xiaohongshu.com/discovery/item/6a37d792000000001603d1e6?source=webshare&amp;amp;xhsshare=pc_web&amp;amp;xsec_token=ABEmPNwSKHOSHrHX08VO30nnBUZ5w6x2VLUGySeBZ8FzY=&amp;amp;xsec_source=pc_share&lt;/p&gt;
&lt;p&gt;动画源文件是 GIF，有 28 帧，每帧 124×124 像素。ESP32-S3 的 Flash 够大，但直接把 GIF 解码放在 &lt;code&gt;loop&lt;/code&gt; 里跑会拖垮性能，而且 &lt;code&gt;drawBitmap&lt;/code&gt; 逐像素绘制在 240×135 的屏幕上帧率很难看。我的做法是在 PC 上预先把 GIF 转成 RGB565 格式，存到 C 头文件里，每帧一个 &lt;code&gt;const uint16_t&lt;/code&gt; 数组，加上 &lt;code&gt;PROGMEM&lt;/code&gt; 修饰放进 Flash。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;lt;M5Cardputer.h&amp;gt;
#include &amp;quot;salary_cat_animation.h&amp;quot;

constexpr uint16_t kBackground = TFT_WHITE;
constexpr uint32_t kFrameTimeMs = 40;

uint32_t lastFrameAt = 0;
uint8_t frameIndex = 0;

void drawFrame(uint8_t index) {
  const int32_t x = (M5Cardputer.Display.width() - SALARY_CAT_FRAME_WIDTH) / 2;
  const int32_t y = (M5Cardputer.Display.height() - SALARY_CAT_FRAME_HEIGHT) / 2;
  const uint16_t* frame = SALARY_CAT_FRAMES[index];

  M5Cardputer.Display.pushImage(x, y, SALARY_CAT_FRAME_WIDTH,
                                SALARY_CAT_FRAME_HEIGHT, frame);
}

void setup() {
  auto cfg = M5.config();
  M5Cardputer.begin(cfg, true);
  M5Cardputer.Display.setRotation(1);
  M5Cardputer.Display.setBrightness(180);
  M5Cardputer.Display.fillScreen(kBackground);

  drawFrame(frameIndex);
  lastFrameAt = millis();
}

void loop() {
  M5Cardputer.update();

  const uint32_t now = millis();
  if (now - lastFrameAt &amp;gt;= kFrameTimeMs) {
    lastFrameAt = now;
    frameIndex = (frameIndex + 1) % SALARY_CAT_FRAME_COUNT;
    drawFrame(frameIndex);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里用了 &lt;code&gt;pushImage&lt;/code&gt; 而不是逐像素绘制，直接把一整块 RGB565 数据推到屏幕驱动里。28 帧全彩动画的头文件体积不小，烧录时需要把分区表改成 8MB Flash 配置，否则默认的 1.2MB 应用分区装不下。&lt;/p&gt;</content:encoded></item><item><title>记某人工智能网络安全应用测试活动</title><link>https://saurlax.com/blog/ai-cybersecurity-testing/</link><guid isPermaLink="true">https://saurlax.com/blog/ai-cybersecurity-testing/</guid><description>最近参加了某人工智能网络安全应用测试活动，活动提供了双卡 Ascend 910B，要求离线断网环境下完成安全系统的代码审计和渗透测试。思来想去打算使用 Qwen3.6 + Opencode 的方式来完...</description><pubDate>Wed, 17 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近参加了某人工智能网络安全应用测试活动，活动提供了双卡 Ascend 910B，要求离线断网环境下完成安全系统的代码审计和渗透测试。思来想去打算使用 Qwen3.6 + Opencode 的方式来完成代码审计和渗透测试，下面是我的环境搭建过程。&lt;/p&gt;
&lt;h2&gt;配置环境&lt;/h2&gt;
&lt;p&gt;首先查看系统信息和 NPU 信息：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;[root@hostname ~]# neofetch --off
root@hostname
-------------------------------
OS: openEuler 22.03 (LTS-SP4) aarch64
Host: KVM Virtual Machine virt-6.2
Kernel: 5.10.0-268.0.0.171.oe2203sp4.aarch64
Uptime: 3 days, 20 hours, 12 mins
Packages: 777 (rpm)
Shell: bash 5.1.8
Resolution: 1024x768
Terminal: /dev/pts/0
CPU: (44) @ 2.400GHz
GPU: 02:05.0 Virtio: Virtio GPU
Memory: 1500MiB / 385480MiB

[root@hostname ~]# npu-smi info
+------------------------------------------------------------------------------------------------+
| npu-smi 25.5.1 Version: 25.5.1                                                                 |
+---------------------------+---------------+----------------------------------------------------+
| NPU Name                  | Health        | Power(W) Temp(C) Hugepages-Usage(page)             |
| Chip                      | Bus-Id        | AICore(%) Memory-Usage(MB) HBM-Usage(MB)           |
+===========================+===============+====================================================+
| 4                         | 910B2         | OK      | 88.4     42           0 / 0              |
| 0                         | 0000:07:00.0  | 0       | 0                | 3202 / 65536          |
+===========================+===============+====================================================+
| 5                         | 910B2         | OK      | 86.2     41           0 / 0              |
| 0                         | 0000:08:00.0  | 0       | 0                | 3203 / 65536          |
+===========================+===============+====================================================+
+---------------------------+---------------+----------------------------------------------------+
| NPU Chip                  | Process id    | Process name | Process memory(MB)                  |
+===========================+===============+====================================================+
| No running processes found in NPU 4                                                            |
+===========================+===============+====================================================+
| No running processes found in NPU 5                                                            |
+===========================+===============+====================================================+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到两张 Ascend 910B 卡都正常工作，接下来初始化数据盘：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;[root@hostname ~]# fdisk -l
Disk /dev/vda: 445 GiB, 477815111680 bytes, 933232640 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: gpt
Disk identifier: 5C41065C-1B0B-4D88-A1B0-099219C70712
Device         Start      End   Sectors Size Type
/dev/vda1      2048     411647    409600 200M EFI System
/dev/vda2    411648   2508799   2097152   1G Linux filesystem
/dev/vda3    2508800   2512895     4096   2M BIOS boot
/dev/vda4    2512896 933230591 930717696 443.8G Linux LVM

Disk /dev/vdb: 512 GiB, 549755813888 bytes, 1073741824 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes

Disk /dev/mapper/vg_sys-lv_root: 355.04 GiB, 381216096256 bytes, 744562688 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes

[root@hostname ~]# mkfs.ext4 /dev/vdb
mke2fs 1.46.4 (18-Aug-2021)
Discarding device blocks: done
Creating filesystem with 134217728 4k blocks and 33554432 inodes
Filesystem UUID: c374cecb-28bc-4be5-82b7-1576a028f299
Superblock backups stored on blocks: 32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208, 4096000, 7962624, 11239424, 20480000, 23887872, 71663616, 78675968, 102400000
Allocating group tables: done
Writing inode tables: done
Creating journal (262144 blocks): done
Writing superblocks and filesystem accounting information: done

[root@hostname ~]# mkdir /data

[root@hostname ~]# mount /dev/vdb /data

[root@hostname ~]# df -h
Filesystem      Size  Used Avail Use% Mounted on
devtmpfs      4.0M     0  4.0M   0% /dev
tmpfs        189G     0  189G   0% /dev/shm
tmpfs         76G  1.3M   76G   1% /run
tmpfs        4.0M     0  4.0M   0% /sys/fs/cgroup
/dev/mapper/vg_sys-lv_root  355G  7.9G  347G   3% /
tmpfs        189G     0  189G   0% /tmp
/dev/vda2    1014M  160M  855M  16% /boot
/dev/vda1     200M  6.5M  194M   4% /boot/efi
tmpfs         38G     0   38G   0% /run/user/0
/dev/vdb      503G   28K  478G   1% /data
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完成之后就可以准备下载模型了，这里我们直接使用 Qwen3.6-35B-A3B 的基础模型版本：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install modelscope
modelscope download --model Qwen/Qwen3.6-35B-A3B --local-dir /data/models/Qwen3.6-35B-A3B
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后启动 vllm-ascend 插件版本，这里我已经配置好优化参数了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker run -d \
  --name vllm-ascend \
  --net=host \
  --shm-size=64g \
  --device /dev/davinci4 \
  --device /dev/davinci5 \
  --device /dev/davinci_manager \
  --device /dev/devmm_svm \
  --device /dev/hisi_hdc \
  -v /usr/local/dcmi:/usr/local/dcmi \
  -v /usr/local/Ascend/driver/tools/hccn_tool:/usr/local/Ascend/driver/tools/hccn_tool \
  -v /usr/local/bin/npu-smi:/usr/local/bin/npu-smi \
  -v /usr/local/Ascend/driver/lib64/:/usr/local/Ascend/driver/lib64/ \
  -v /usr/local/Ascend/driver/version.info:/usr/local/Ascend/driver/version.info \
  -v /etc/ascend_install.info:/etc/ascend_install.info \
  -v /data:/data \
  quay.io/ascend/vllm-ascend:v0.21.0rc1-openeuler \
  vllm serve /data/models/Qwen3.6-35B-A3B \
  --served-model-name Qwen3.6-35B-A3B \
  --tensor-parallel-size 2 \
  --trust-remote-code \
  --max-model-len 65536 \
  --reasoning-parser qwen3 \
  --enable-auto-tool-choice \
  --tool-call-parser qwen3_coder \
  --speculative-config &apos;{&amp;quot;method&amp;quot;:&amp;quot;qwen3_next_mtp&amp;quot;,&amp;quot;num_speculative_tokens&amp;quot;:2}&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--shm-size=64g&lt;/code&gt; 是指定共享内存大小，避免模型加载时出现内存不足的问题&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--tensor-parallel-size 2&lt;/code&gt; 是指定张量并行的大小&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--max-model-len 65536&lt;/code&gt; 是指定模型的上下文长度&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--reasoning-parser qwen3&lt;/code&gt; 是指定推理解析器&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--enable-auto-tool-choice&lt;/code&gt; 是启用自动工具选择&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--tool-call-parser qwen3_coder&lt;/code&gt; 是指定工具调用解析器&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--speculative-config &apos;{&amp;quot;method&amp;quot;:&amp;quot;qwen3_next_mtp&amp;quot;,&amp;quot;num_speculative_tokens&amp;quot;:2}&apos;&lt;/code&gt; 用于开启 MTP 推理模式，提升推理速度&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;启动后可以通过以下命令查看日志和进入容器进行交互：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 查看日志
docker logs -f vllm-ascend
# 打开 vllm chat 交互
docker exec -it vllm-ascend vllm chat
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;配置 Agent&lt;/h2&gt;
&lt;p&gt;首先安装 nvm 和 Node.js，我们使用 Node.js 24 LTS 版本。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 下载并安装 nvm：
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
# 代替重启 shell
\. &amp;quot;$HOME/.nvm/nvm.sh&amp;quot;
# 下载并安装 Node.js：
nvm install 24
# 验证 Node.js 版本：
node -v # Should print &amp;quot;v24.17.0&amp;quot;.
# 验证 npm 版本：
npm -v # Should print &amp;quot;11.13.0&amp;quot;.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Codex&lt;/h3&gt;
&lt;p&gt;目前 vLLM 对 responses 的支持还是实验性功能，这里只是 Codex 的样例，最后我们选择的是下面的 Opencode 来完成 Agent 的配置。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install -g @openai/codex
cat &amp;gt; ~/.codex/config.toml &amp;lt;&amp;lt;EOL
model_provider = &amp;quot;custom&amp;quot;
model = &amp;quot;Qwen3.6-35B-A3B&amp;quot;

[model_providers.custom]
name = &amp;quot;custom&amp;quot;
wire_api = &amp;quot;responses&amp;quot;
base_url = &amp;quot;http://localhost:8000/v1&amp;quot;

[model_metadata.Qwen3.6-35B-A3B]
context_window = 65536
max_output_tokens = 16384
supports_streaming = true
supports_tool_calls = true
EOL
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Opencode&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm i -g opencode-ai
cd /data/workspace
cat &amp;gt; opencode.json &amp;lt;&amp;lt;EOL
{
  &amp;quot;$schema&amp;quot;: &amp;quot;https://opencode.ai/config.json&amp;quot;,
  &amp;quot;provider&amp;quot;: {
    &amp;quot;local&amp;quot;: {
      &amp;quot;npm&amp;quot;: &amp;quot;@ai-sdk/openai-compatible&amp;quot;,
      &amp;quot;options&amp;quot;: {
        &amp;quot;baseURL&amp;quot;: &amp;quot;http://localhost:8000/v1&amp;quot;
      },
      &amp;quot;models&amp;quot;: {
        &amp;quot;Qwen3.6-35B-A3B&amp;quot;: {}
      }
    }
  }
}
EOL
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下一步就可以爽爽安装各种 Skills、MCP 和插件了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone https://gitcode.com/gh_mirrors/cv/cve-bin-tool.git
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还有个 &lt;code&gt;opencode-goal&lt;/code&gt;，在 &lt;code&gt;opencode.json&lt;/code&gt; 中添加以下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;plugin&amp;quot;: [&amp;quot;opencode-plugin-goal&amp;quot;]
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title>Volta：下一代 Node.js 版本管理器</title><link>https://saurlax.com/blog/volta/</link><guid isPermaLink="true">https://saurlax.com/blog/volta/</guid><description>之前一直在使用 nvm，最近切到了 Volta。nvm 满足了基本需求，但每次开终端手动 `nvm use`、CI 里 `.nvmrc` 不自动识别、切版本后全局 CLI 工具挂掉——这些算不上大问题...</description><pubDate>Mon, 15 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;之前一直在使用 nvm，最近切到了 Volta。nvm 满足了基本需求，但每次开终端手动 &lt;code&gt;nvm use&lt;/code&gt;、CI 里 &lt;code&gt;.nvmrc&lt;/code&gt; 不自动识别、切版本后全局 CLI 工具挂掉——这些算不上大问题却反复出现的摩擦，让我开始找替代方案。&lt;/p&gt;
&lt;h2&gt;旧方案回顾&lt;/h2&gt;
&lt;p&gt;nvm 是最早的 Node 版本管理器，通过修改 &lt;code&gt;PATH&lt;/code&gt; 指向不同版本目录。每次打开终端得手动切，或者配置 shell 脚本让它读取 &lt;code&gt;.nvmrc&lt;/code&gt;。nvm 是 bash 写的，启动脚本如果在每次命令执行时都检测 &lt;code&gt;.nvmrc&lt;/code&gt;，累积延迟相当烦人。&lt;/p&gt;
&lt;p&gt;fnm 是 Rust 写的 nvm 替代品，速度显著更快，但逻辑一样：读 &lt;code&gt;.node-version&lt;/code&gt; 或 &lt;code&gt;.nvmrc&lt;/code&gt;，手动 &lt;code&gt;fnm use&lt;/code&gt;。解决了性能，没解决需要手动操作的设计问题。&lt;/p&gt;
&lt;p&gt;n 更轻量，只有全局版本的概念，不提供项目级自动切换。&lt;/p&gt;
&lt;p&gt;这三者的共同短板：版本切换需要用户主动执行命令。&lt;/p&gt;
&lt;h2&gt;Volta 的自动路由&lt;/h2&gt;
&lt;p&gt;Volta 的思路完全不同——你不需要告诉它用什么版本，它自己根据项目自动选。&lt;/p&gt;
&lt;p&gt;实现方式是 &lt;code&gt;PATH&lt;/code&gt; 中放置一批轻量 shim 代理（&lt;code&gt;node&lt;/code&gt;、&lt;code&gt;npm&lt;/code&gt;、&lt;code&gt;yarn&lt;/code&gt;、&lt;code&gt;pnpm&lt;/code&gt; 等）。敲 &lt;code&gt;node&lt;/code&gt; 时实际执行的是 shim，shim 检测当前目录，判断是否在某个项目里——普通目录走默认版本，项目目录树下读取 &lt;code&gt;package.json&lt;/code&gt; 里的 &lt;code&gt;volta&lt;/code&gt; 字段择优切换。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;volta pin node@22&lt;/code&gt; 会在 &lt;code&gt;package.json&lt;/code&gt; 中写入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;&amp;quot;volta&amp;quot;: {
  &amp;quot;node&amp;quot;: &amp;quot;22.22.3&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此后 cd 进项目，&lt;code&gt;node&lt;/code&gt;、&lt;code&gt;npm&lt;/code&gt;、&lt;code&gt;pnpm&lt;/code&gt; 全部自动切到指定版本。团队里装了 Volta 的人不需要额外操作，版本一致性天然保证。包管理器也能 pin，&lt;code&gt;volta pin pnpm@9&lt;/code&gt; 锁死 pnpm 版本。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;volta run --node 20 node -e &amp;quot;...&amp;quot;&lt;/code&gt; 可以临时用某个版本执行命令，不影响当前环境。&lt;/p&gt;
&lt;h2&gt;全局 CLI 工具的隔离&lt;/h2&gt;
&lt;p&gt;用 nvm 切 Node 版本后，之前全局装的 CLI 可能挂掉——那些工具绑定了旧的 Node 二进制路径。Volta 让每个全局工具安装时就固定好依赖的 Node 引擎版本：&lt;code&gt;pnpm add -g tsx&lt;/code&gt; 时记住这个工具在 Node 22 下构建，之后切到 Node 18 也不影响 tsx 的运行。&lt;/p&gt;
&lt;p&gt;另外在项目里执行 &lt;code&gt;npm run build&lt;/code&gt; 或 &lt;code&gt;pnpm run dev&lt;/code&gt; 时，Volta 会自动隐藏全局工具链的 shim，避免项目脚本意外依赖全局安装的包。&lt;/p&gt;
&lt;h2&gt;对比&lt;/h2&gt;
&lt;p&gt;|                  | nvm   | fnm   | n     | Volta   |
| ---------------- | ----- | ----- | ----- | ------- |
| 语言             | bash  | Rust  | bash  | Rust    |
| 项目级版本绑定   | &lt;code&gt;.nvmrc&lt;/code&gt; 需配置 | &lt;code&gt;.nvmrc&lt;/code&gt; 需配置 | 不支持 | &lt;code&gt;package.json&lt;/code&gt; 自动 |
| 包管理器版本管理 | 不支持 | 不支持 | 不支持 | 原生支持 |
| 全局工具隔离     | 无     | 无     | 无     | 有       |
| 启动速度         | 慢     | 快     | 快     | 快       |
| 跨平台           | 仅 Unix | 全平台 | 仅 Unix | 全平台   |&lt;/p&gt;</content:encoded></item><item><title>Expo 项目在 GitHub Actions 上打包 Android Release</title><link>https://saurlax.com/blog/expo-github-actions/</link><guid isPermaLink="true">https://saurlax.com/blog/expo-github-actions/</guid><description>最近把 Expo 项目的 Android Release 打包流程迁移到了 GitHub Actions 上。原本以为只是写个 workflow 的事，结果一路踩了不少坑。这里把整个过程里遇到的问题和...</description><pubDate>Tue, 02 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近把 Expo 项目的 Android Release 打包流程迁移到了 GitHub Actions 上。原本以为只是写个 workflow 的事，结果一路踩了不少坑。这里把整个过程里遇到的问题和解决方案整理出来，供有类似需求的同学参考。&lt;/p&gt;
&lt;h2&gt;背景与目标&lt;/h2&gt;
&lt;p&gt;项目是一个使用 Expo 管理的 React Native 应用，采用 monorepo 结构（pnpm workspace）。希望实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;推送 tag 触发 release 时，自动构建并上传 APK&lt;/li&gt;
&lt;li&gt;支持手动触发 workflow_dispatch，方便日常测试&lt;/li&gt;
&lt;li&gt;构建产物既能通过 GitHub Release 下载，也能在 Actions 页面直接拿 Artifacts&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Node.js 版本警告&lt;/h2&gt;
&lt;p&gt;一开始用的是 &lt;code&gt;actions/setup-node@v3&lt;/code&gt;，没多久 GitHub Actions 开始发通知：&lt;strong&gt;Node 20 即将被弃用，建议迁移到 Node 22 或 24&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;当时第一反应是去改 &lt;code&gt;setup-node&lt;/code&gt; 的 &lt;code&gt;node-version&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;- name: Setup Node
  uses: actions/setup-node@v6
  with:
    node-version: &amp;quot;20&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但这里有个容易混淆的点：&lt;code&gt;actions/setup-node&lt;/code&gt; 里的 &lt;code&gt;node-version&lt;/code&gt; 只影响 workflow 里后续执行的 &lt;code&gt;node&lt;/code&gt; / &lt;code&gt;pnpm&lt;/code&gt; 命令（比如 &lt;code&gt;pnpm install&lt;/code&gt;、Expo CLI 等），&lt;strong&gt;不影响 action 自身的运行时&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;真正导致 warning 的原因是：&lt;code&gt;android-actions/setup-android@v3&lt;/code&gt; 这个 action 自身内部还基于 Node 20 运行。这是 action 作者发布时绑定的运行时，不会因为你在 &lt;code&gt;setup-node&lt;/code&gt; 里改成 22 或 24 就自动变掉。&lt;/p&gt;
&lt;p&gt;解决方案有两层：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;短期兼容&lt;/strong&gt;：添加环境变量 &lt;code&gt;FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true&lt;/code&gt;，强制 GitHub 上的 JavaScript actions 自身用 Node 24 跑&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;长期修复&lt;/strong&gt;：升级 action 到支持 Node 24 的版本&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;android-actions/setup-android 版本升级&lt;/h2&gt;
&lt;p&gt;顺着上面的方向查下去，发现 &lt;code&gt;android-actions/setup-android&lt;/code&gt; 其实已经有 v4.0.1 了，而我一直停留在 v3。&lt;/p&gt;
&lt;p&gt;升级很简单，直接改版本号：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;- name: Setup Android SDK
  uses: android-actions/setup-android@v4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;升级到 v4 后，Node 版本的 warning 就彻底消失了，&lt;code&gt;FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true&lt;/code&gt; 这个兼容开关也可以去掉了。&lt;/p&gt;
&lt;h2&gt;APK 架构与体积优化&lt;/h2&gt;
&lt;p&gt;第一次构建出来的 APK 只有 71MB，感觉还不错。但仔细看配置，发现 &lt;code&gt;gradle.properties&lt;/code&gt; 里写的是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-properties&quot;&gt;reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这意味着构建时会把 &lt;strong&gt;armeabi-v7a、arm64-v8a、x86、x86_64&lt;/strong&gt; 全部打包进去。其中 x86 / x86_64 主要是给模拟器用的，真机发行包其实不需要它们。&lt;/p&gt;
&lt;p&gt;如果你的目标是「发给真机安装，尽量小」，通常建议改成：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-properties&quot;&gt;reactNativeArchitectures=arm64-v8a
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果还想兼容少量老 32 位 Android 真机，可以用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-properties&quot;&gt;reactNativeArchitectures=armeabi-v7a,arm64-v8a
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;改完之后 APK 体积会明显下降，因为不再浪费空间在模拟器 ABI 上。&lt;/p&gt;
&lt;p&gt;另外，还可以在 &lt;code&gt;gradle.properties&lt;/code&gt; 里开启混淆和资源收缩，进一步减小体积：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-properties&quot;&gt;android.enableMinifyInReleaseBuilds=true
android.enableShrinkResourcesInReleaseBuilds=true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以及，如果项目里没用到 GIF 或 WebP，可以把 &lt;code&gt;app.json&lt;/code&gt; 里对应的解码支持关掉，也能省一点点体积：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;expo&amp;quot;: {
    &amp;quot;gif&amp;quot;: { &amp;quot;enabled&amp;quot;: false },
    &amp;quot;webp&amp;quot;: { &amp;quot;enabled&amp;quot;: false }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;GitHub Release 权限问题&lt;/h2&gt;
&lt;p&gt;在配置 &lt;code&gt;softprops/action-gh-release@v2&lt;/code&gt; 上传 APK 到 Release 时，遇到了这样一个错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;⚠️ Unexpected error fetching GitHub release for tag refs/tags/v0.1.0-alpha:
HttpError: Resource not accessible by integration
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原因是：&lt;code&gt;action-gh-release&lt;/code&gt; 需要 &lt;code&gt;contents: write&lt;/code&gt; 权限来更新 Release 并上传附件，但 workflow 默认拿到的 &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; 权限是只读的。&lt;/p&gt;
&lt;p&gt;解决方法是在 workflow 文件顶部添加权限声明：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;permissions:
  contents: write
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;加完之后，Release 的更新和附件上传就能正常进行了。&lt;/p&gt;
&lt;h2&gt;非 Release 触发时的产物获取&lt;/h2&gt;
&lt;p&gt;除了通过 Release 触发构建，日常测试中更常用的是手动触发 &lt;code&gt;workflow_dispatch&lt;/code&gt;。这种情况下，构建产物不会附加到 GitHub Release，但可以通过 Artifacts 下载。&lt;/p&gt;
&lt;p&gt;在 workflow 里加上这一步：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;- name: Upload APK artifact
  uses: actions/upload-artifact@v7
  with:
    name: app-release-apk
    path: apps/mobile/android/app/build/outputs/apk/release/app-release.apk
    if-no-files-found: error
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;构建成功后，在 &lt;strong&gt;GitHub 仓库页面 → Actions → 点进对应的 workflow run → 页面底部或右侧 Artifacts&lt;/strong&gt; 里就能看到并下载 &lt;code&gt;app-release-apk&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;最终的 Workflow 文件&lt;/h2&gt;
&lt;p&gt;经过上面这些调整，最终的 &lt;code&gt;android-release-build.yml&lt;/code&gt; 是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;name: android-release-build

on:
  workflow_dispatch:
  release:
    types: [published]

permissions:
  contents: write

jobs:
  build-release-apk:
    runs-on: ubuntu-latest
    env:
      EXPO_NO_METRO_WORKSPACE_ROOT: &amp;quot;1&amp;quot;
    defaults:
      run:
        working-directory: apps/mobile/android

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Setup Java
        uses: actions/setup-java@v5
        with:
          distribution: temurin
          java-version: &amp;quot;17&amp;quot;
          cache: gradle

      - name: Setup Android SDK
        uses: android-actions/setup-android@v4

      - name: Setup pnpm
        uses: pnpm/action-setup@v6
        with:
          version: 10.17.1

      - name: Setup Node
        uses: actions/setup-node@v6
        with:
          node-version: &amp;quot;20&amp;quot;
          cache: &amp;quot;pnpm&amp;quot;
          cache-dependency-path: pnpm-lock.yaml

      - name: Install JS dependencies
        run: pnpm install --frozen-lockfile
        working-directory: ${{ github.workspace }}

      - name: Grant Gradle wrapper permission
        run: chmod +x gradlew

      - name: Build release APK
        run: EXPO_NO_METRO_WORKSPACE_ROOT=1 ./gradlew assembleRelease --no-daemon

      - name: Upload APK artifact
        uses: actions/upload-artifact@v7
        with:
          name: app-release-apk
          path: apps/mobile/android/app/build/outputs/apk/release/app-release.apk
          if-no-files-found: error

      - name: Upload APK to GitHub Release
        if: github.event_name == &apos;release&apos;
        uses: softprops/action-gh-release@v2
        with:
          files: apps/mobile/android/app/build/outputs/apk/release/app-release.apk
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;几个值得注意的点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;EXPO_NO_METRO_WORKSPACE_ROOT=1&lt;/code&gt; 是 Expo 在 monorepo 环境下需要的变量，避免 Metro 找不到正确的根目录&lt;/li&gt;
&lt;li&gt;&lt;code&gt;working-directory&lt;/code&gt; 设为 &lt;code&gt;apps/mobile/android&lt;/code&gt;，因为 Gradle 构建是在 Android 原生目录下执行的&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cache-dependency-path&lt;/code&gt; 指向根目录的 &lt;code&gt;pnpm-lock.yaml&lt;/code&gt;，确保 pnpm 的缓存能正确命中&lt;/li&gt;
&lt;li&gt;&lt;code&gt;if-no-files-found: error&lt;/code&gt; 保证如果 APK 没构建出来，workflow 会直接失败，而不是静默跳过&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>洛克王国世界精灵知识问答竞赛题库</title><link>https://saurlax.com/blog/rocom-question/</link><guid isPermaLink="true">https://saurlax.com/blog/rocom-question/</guid><description>&gt; 答案均为第一个选项

1. 当你在野外对精灵使用，救救我动作时，有可能会发生什么事？
   - 精灵会帮我恢复魔力值！
   - 精灵会将我带到附近的魔力之源！
   - 精灵会给我一些可可果！
...</description><pubDate>Mon, 25 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;答案均为第一个选项&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;当你在野外对精灵使用，救救我动作时，有可能会发生什么事？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;精灵会帮我恢复魔力值！&lt;/li&gt;
&lt;li&gt;精灵会将我带到附近的魔力之源！&lt;/li&gt;
&lt;li&gt;精灵会给我一些可可果！&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在野外探索时，想要主动改变天气，该怎么做呢？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用水系精灵激活祈雨器！&lt;/li&gt;
&lt;li&gt;将水系精灵带在身边！&lt;/li&gt;
&lt;li&gt;用水系精灵使用雨天技能！&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;你知道如何快速击破千级盔首领的魔法护盾吗？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用星星魔法攻击他头上的盔甲！&lt;/li&gt;
&lt;li&gt;用星星魔法攻击他的头部！&lt;/li&gt;
&lt;li&gt;用星星魔法攻击他的眼睛！&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;你知道蓝珠天鹅如何进化成黑羽夫人吗？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;蓝珠天鹅的血脉须为恶系血脉！&lt;/li&gt;
&lt;li&gt;为其使用四块恶系技能石！&lt;/li&gt;
&lt;li&gt;为其使用黑魔法！&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;以下哪种特长可以将野外精灵的血量在战斗中精准控制在最低点？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;慈悲为怀！&lt;/li&gt;
&lt;li&gt;怜悯！&lt;/li&gt;
&lt;li&gt;小心谨慎！&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;绿翼鸟有一个超特别的生活习性，请问这个习性是什么？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;喜欢待在洛克头上！&lt;/li&gt;
&lt;li&gt;喜欢和一窝蜂打架！&lt;/li&gt;
&lt;li&gt;喜欢恶作剧！&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;梦游的睡衣纽扣是什么图案呢？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一定是叉号！&lt;/li&gt;
&lt;li&gt;对勾！&lt;/li&gt;
&lt;li&gt;纽扣肯定是圆形的！&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;世界上存在两种石肤蜥的样子，你知道应该观察哪里区分吗？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通过观察尾巴的不同！&lt;/li&gt;
&lt;li&gt;通过观察皮肤颜色的不同！&lt;/li&gt;
&lt;li&gt;通过观察体型的不同！&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;以下哪只精灵是虫系精灵呢？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;爬爬！&lt;/li&gt;
&lt;li&gt;矿晶虫！&lt;/li&gt;
&lt;li&gt;卡卡虫！&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;请问美妙球更容易捕捉到以下哪只精灵？&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;鸭吉吉！&lt;/li&gt;
&lt;li&gt;梦游！&lt;/li&gt;
&lt;li&gt;海枝枝！&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;11&quot;&gt;
&lt;li&gt;水系精灵喜欢下雨天，在下雨天时，水系精灵会获得怎样的增幅？&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;水系技能伤害会增加！&lt;/li&gt;
&lt;li&gt;水系精灵的双抗会增加！&lt;/li&gt;
&lt;li&gt;水系精灵的能耗会降低！&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;12&quot;&gt;
&lt;li&gt;以下哪只精灵的进化条件与精灵性别有关？&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;一窝蜂！&lt;/li&gt;
&lt;li&gt;蓝珠天鹅！&lt;/li&gt;
&lt;li&gt;帽兜娃娃！&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;13&quot;&gt;
&lt;li&gt;从树上掉下来的东西除了可可果以外还会是什么？&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;旋叶虫！&lt;/li&gt;
&lt;li&gt;雪绒鸟！&lt;/li&gt;
&lt;li&gt;小草虫！&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;14&quot;&gt;
&lt;li&gt;幽影树平时会变成什么东西伪装自己？&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;变成木桩！&lt;/li&gt;
&lt;li&gt;变成洛克的样子！&lt;/li&gt;
&lt;li&gt;变成木桶！&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;15&quot;&gt;
&lt;li&gt;你知道身上有爱心图案的岚鸟，其外形是什么颜色吗？&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;也许是绿色！&lt;/li&gt;
&lt;li&gt;似乎是棕色！&lt;/li&gt;
&lt;li&gt;肯定是蓝色！&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;16&quot;&gt;
&lt;li&gt;以下哪只精灵是用漂浮的方式进行日常移动？&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;权杖-II！&lt;/li&gt;
&lt;li&gt;地鼠！&lt;/li&gt;
&lt;li&gt;雪巨人！&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;17&quot;&gt;
&lt;li&gt;以下哪只精灵拥有巨大尾巴？&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;忽幽狸！&lt;/li&gt;
&lt;li&gt;锥尾羊！&lt;/li&gt;
&lt;li&gt;爵士鹿！&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;18&quot;&gt;
&lt;li&gt;在精灵对决中，当对手派出尖嘴狐仙时，用什么系别的技能可以对其进行反制？&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;地系技能！&lt;/li&gt;
&lt;li&gt;草系技能！&lt;/li&gt;
&lt;li&gt;机械系技能！&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;19&quot;&gt;
&lt;li&gt;獠牙猪的两只獠牙有什么妙用？&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;洛克可以坐在上面！&lt;/li&gt;
&lt;li&gt;可以用来生火！&lt;/li&gt;
&lt;li&gt;可以用来挖掘宝箱！&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;20&quot;&gt;
&lt;li&gt;在精灵对决中，洛克将精灵首领化以后会发生什么？&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;处于首领状态时，精灵特性会变化！&lt;/li&gt;
&lt;li&gt;精灵等级将会临时提高！&lt;/li&gt;
&lt;li&gt;精灵的性格将会发生变化！&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;21&quot;&gt;
&lt;li&gt;当你在野外接触到多多喷出的毒雾时，你会看到什么？&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;三只漂浮在空中的鸭吉吉！&lt;/li&gt;
&lt;li&gt;四只围绕在身边的地鼠！&lt;/li&gt;
&lt;li&gt;五只滚来滚去的蒲公英娃娃！&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;22&quot;&gt;
&lt;li&gt;恶水沼泽的乌拉塔在外形上除颜色外，与其他地区的乌拉塔还有什么差异？&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;犄角方向不一样！&lt;/li&gt;
&lt;li&gt;眼睛颜色不一样！&lt;/li&gt;
&lt;li&gt;手掌大小不一样！&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;23&quot;&gt;
&lt;li&gt;在精灵对决时，精灵所受到的异常状态与印记之间有什么差异？&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;印记会被新上场精灵继承！&lt;/li&gt;
&lt;li&gt;印记无法通过天气改变被清除！&lt;/li&gt;
&lt;li&gt;印记无法通过首领化被清除！&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;24&quot;&gt;
&lt;li&gt;当九幽菇使用技能时，其身上的蘑菇会发生什么变化？&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;蘑菇的表情会发生变化！&lt;/li&gt;
&lt;li&gt;蘑菇的大小会发生变化！&lt;/li&gt;
&lt;li&gt;蘑菇的数量会发生变化！&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;25&quot;&gt;
&lt;li&gt;在精灵对决中，若我方精灵获得了蓄势印记，此时该精灵可以获得什么增益？&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;精灵的攻击类技能威力增加！&lt;/li&gt;
&lt;li&gt;精灵的防御类技能能耗减少！&lt;/li&gt;
&lt;li&gt;精灵的防御属性得到提升！&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;26&quot;&gt;
&lt;li&gt;以下哪个场景可以体现精灵对决中精灵系别所给予精灵的增益？&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;火系精灵免疫灼烧状态！&lt;/li&gt;
&lt;li&gt;火系精灵会在雨天天气伤害降低！&lt;/li&gt;
&lt;li&gt;火系精灵可以点燃火堆！&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;27&quot;&gt;
&lt;li&gt;什么精灵在进化后，眼睛的数量会增加？&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;哭哭菇！&lt;/li&gt;
&lt;li&gt;拉特！&lt;/li&gt;
&lt;li&gt;尖角蜘蛛！&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;28&quot;&gt;
&lt;li&gt;你知道如何快速击破伊兰亚龙首领的魔法护盾吗？&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;用星星魔法攻击他的头部！&lt;/li&gt;
&lt;li&gt;用星星魔法攻击它的尾巴！&lt;/li&gt;
&lt;li&gt;用星星魔法攻击他的鳞片！&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;29&quot;&gt;
&lt;li&gt;为什么在精灵对决时，对方会派上一个木桶？&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;因为绿草精灵的特性！&lt;/li&gt;
&lt;li&gt;因为草系宠物的防御系技能！&lt;/li&gt;
&lt;li&gt;因为洛克施展了魔法！&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;30&quot;&gt;
&lt;li&gt;在世界上，格兰一族有一个十分响亮的称号，你知道是什么吗？&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;格兰大帝！&lt;/li&gt;
&lt;li&gt;格兰船长！&lt;/li&gt;
&lt;li&gt;格兰领主！&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;31&quot;&gt;
&lt;li&gt;兽花蕾会根据自身血脉不同而变化自己的特性，那么当兽花蕾的血脉为电系，会带来什么效果？&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;速度会得到提升！&lt;/li&gt;
&lt;li&gt;技能会附带迸发效果！&lt;/li&gt;
&lt;li&gt;技能的能耗会减少！&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;32&quot;&gt;
&lt;li&gt;以下哪只精灵不能进化呢？&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;或许是公平鸽！&lt;/li&gt;
&lt;li&gt;应该是优优！&lt;/li&gt;
&lt;li&gt;肯定是气球猫！&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;33&quot;&gt;
&lt;li&gt;若使用星星魔法瞄准野外的卡瓦重，此时会发生什么？&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;卡瓦重会冲我唱歌！&lt;/li&gt;
&lt;li&gt;卡瓦重会钻地逃跑！&lt;/li&gt;
&lt;li&gt;卡瓦重会原地转圈！&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;34&quot;&gt;
&lt;li&gt;什么精灵在野外会将自己同进化链精灵丢向洛克，随后自己跑掉？&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;卷毛鸭！&lt;/li&gt;
&lt;li&gt;鸭吉吉！&lt;/li&gt;
&lt;li&gt;恶魔叮！&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;35&quot;&gt;
&lt;li&gt;晕晕鸡应该如何进化成舞者鸡呢？&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;和晕晕鸡一起击败三只恶系精灵！&lt;/li&gt;
&lt;li&gt;使用水系精灵血脉药剂！&lt;/li&gt;
&lt;li&gt;和晕晕鸡一起击败五只虫系精灵！&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;36&quot;&gt;
&lt;li&gt;在精灵对决中，以下哪种方法可以快速触发迸发效果？&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;使用带有返场效果的技能！&lt;/li&gt;
&lt;li&gt;使用带有连击效果的技能！&lt;/li&gt;
&lt;li&gt;使用带有打断效果的技能！&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;37&quot;&gt;
&lt;li&gt;外形是黄色的小星光，他的胸口是什么形状？&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;或许是五角星！&lt;/li&gt;
&lt;li&gt;记得是六芒星！&lt;/li&gt;
&lt;li&gt;肯定是一个闪电形状！&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;38&quot;&gt;
&lt;li&gt;以下哪只精灵可以在入场回合释放两次技能？&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;酷拉！&lt;/li&gt;
&lt;li&gt;梦想三三！&lt;/li&gt;
&lt;li&gt;皇家狮鹫！&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;39&quot;&gt;
&lt;li&gt;请问棕色的丢丢，它的系别除了草系还有什么系别？&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;地系！&lt;/li&gt;
&lt;li&gt;电系！&lt;/li&gt;
&lt;li&gt;丢丢只有一种属性！&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;40&quot;&gt;
&lt;li&gt;你知道怖哭菇周围都会围绕什么颜色的气体吗？&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;肯定是紫色！&lt;/li&gt;
&lt;li&gt;应该是蓝色！&lt;/li&gt;
&lt;li&gt;貌似是黑色！&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;41&quot;&gt;
&lt;li&gt;在精灵对决中，以下哪只精灵可以在离场时回复能量？&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;电球咩咩！&lt;/li&gt;
&lt;li&gt;雪巨人！&lt;/li&gt;
&lt;li&gt;小星光！&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;42&quot;&gt;
&lt;li&gt;如何才能让果冻进化成抹茶布丁？&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;果冻的血脉须为草系血脉！&lt;/li&gt;
&lt;li&gt;使用抹茶粉！&lt;/li&gt;
&lt;li&gt;为其佩戴上风眠省徽章！&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;43&quot;&gt;
&lt;li&gt;海枝枝这么可爱的精灵，它在世界上一共存在几种形态呢？&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;大概是四种！&lt;/li&gt;
&lt;li&gt;应该有三种！&lt;/li&gt;
&lt;li&gt;也许是五种！&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;44&quot;&gt;
&lt;li&gt;在精灵对决中，什么精灵的特性可以为下一只出场的精灵提高双防？&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;椰浆布丁！&lt;/li&gt;
&lt;li&gt;水泡壳！&lt;/li&gt;
&lt;li&gt;獠牙猪！&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;45&quot;&gt;
&lt;li&gt;以下哪只精灵是萌系精灵？&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;月亮陀！&lt;/li&gt;
&lt;li&gt;小黑猫！&lt;/li&gt;
&lt;li&gt;脆筒甜甜！&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;46&quot;&gt;
&lt;li&gt;若是在彼得大道的眠枭庇护所里放置地鼠果实，那么地鼠的外观会发生什么显著变化？&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;地鼠的耳朵会变大！&lt;/li&gt;
&lt;li&gt;地鼠的眼睛会变成蓝色！&lt;/li&gt;
&lt;li&gt;地鼠的尾巴会变成蓝色！&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;47&quot;&gt;
&lt;li&gt;围绕祭礼巨像的三颗宝珠都是什么颜色？&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;红色，蓝色和绿色！&lt;/li&gt;
&lt;li&gt;红色，蓝色和黄色！&lt;/li&gt;
&lt;li&gt;红色，绿色和黄色！&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;48&quot;&gt;
&lt;li&gt;有一只精灵，它的特征为水系别没有首领化，共有三阶进化，请问他是？&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;龙鱼！&lt;/li&gt;
&lt;li&gt;翡翠水母！&lt;/li&gt;
&lt;li&gt;千棘盔！&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;49&quot;&gt;
&lt;li&gt;你知道镜面相框可以对精灵造成什么影响吗？&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;修正精灵性格的负面影响！&lt;/li&gt;
&lt;li&gt;增加精灵个体资质！&lt;/li&gt;
&lt;li&gt;增加精灵成长值！&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;50&quot;&gt;
&lt;li&gt;在精灵养成中，什么物品可以增加精灵个体资质？&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;能力钥匙！&lt;/li&gt;
&lt;li&gt;血脉药剂！&lt;/li&gt;
&lt;li&gt;镜面相框！&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>What-if Studio 与视频生成工作流</title><link>https://saurlax.com/blog/what-if-studio/</link><guid isPermaLink="true">https://saurlax.com/blog/what-if-studio/</guid><description>前段时间参加南客松 S2 黑客松，和队友一起做了 [What-if Studio](https://whatifstudio.saurlax.com)：输入一句&quot;我不接受这个结局&quot;，系统会组建一个虚拟...</description><pubDate>Fri, 22 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;前段时间参加南客松 S2 黑客松，和队友一起做了 &lt;a href=&quot;https://whatifstudio.saurlax.com&quot;&gt;What-if Studio&lt;/a&gt;：输入一句&amp;quot;我不接受这个结局&amp;quot;，系统会组建一个虚拟剧组，几个导演角色在画布里讨论、争吵、提案、分工，最后交付一段可播放的平行结局短片。思路很直接——很多人看完一个故事之后最强烈的感受往往集中在某个节点本可以是另一个走向，这种感觉最后被具象成一个能操作的产品，上线之后也确实拿到了不错的方向反馈。&lt;/p&gt;
&lt;p&gt;实现上没走一句话进、一段视频出的黑盒路线，而是把前端做成了一座会动的片场：导演室、主舞台、剪辑区、录音区、素材区全在一张 D3 画布里，导演们陆续进场，讨论内容通过 SSE 实时推，用户可以自由缩放、旁观、查看角色卡片，到了需要素材的环节弹出任务卡，最后进入视频生成。后端是 FastAPI + SQLAlchemy + SQLite，对象流按 &lt;code&gt;Project&lt;/code&gt;、&lt;code&gt;Session&lt;/code&gt;、&lt;code&gt;VideoJob&lt;/code&gt; 分为三层，讨论流和任务流各走独立的 SSE 通道，多智能体用 AutoGen 的 &lt;code&gt;RoundRobinGroupChat&lt;/code&gt; 搭了一套导演会，视频生成接了 SiliconFlow 上的 Wan。整条 demo 链路闭环跑通了，方向在黑客松场景下也站得住。&lt;/p&gt;
&lt;p&gt;但做完之后越来越觉得，这个 demo 离真正可用还差得远。差距不在模型——视频生成这件事真正难的地方几乎都不在最后那下模型调用，而在前面的拆解、约束、资产沉淀，以及后面的镜头回修与路由决策。这篇文章主要聊的并不只是 What-if Studio 本身，更多是做完它之后围绕视频生成工作流展开的调研和思考，以及下一代应该往哪走。&lt;/p&gt;
&lt;h2&gt;起点&lt;/h2&gt;
&lt;p&gt;先说 What-if Studio 当前做到了什么程度。&lt;/p&gt;
&lt;h3&gt;剧组即界面&lt;/h3&gt;
&lt;p&gt;聊天框在很多 AIGC 产品里被当成唯一的交互形态，但它把创作过程压缩得过于扁平了——用户只看到一句需求和一个结果，中间的判断、妥协、风格冲突全被吞掉。What-if Studio 前端选了另一条路，把 Agent 做成一个可浏览的片场，这表面上是展示层的选择，背后却非常工程化：只要系统需要承载多个 Agent、多个阶段、多个异步任务，就早晚得有一个能解释当前状态的界面。把这层从一开始就建出来，后面再加镜头规划、资产锁定、失败重试、人工确认节点的时候界面就不会塌。目前前端已经支持了 Agent 运行态可视化、闲聊引擎、剧本审阅弹窗、视频生成进度这些交互，最基本的可观测性算是做出来了。&lt;/p&gt;
&lt;h3&gt;多智能体的价值&lt;/h3&gt;
&lt;p&gt;现在的讨论链路是几类角色分工协作，而不是单 Agent 独白。AutoGen 里搭了五个 Agent：Narrative Director 偏叙事逻辑，Visual Director 偏镜头和视觉结构，Sound Director 偏声音设计，Material Director 偏素材选择，Critic 负责收束成 Markdown 脚本并以 &lt;code&gt;FINAL_JSON&lt;/code&gt; 输出结构化结果。每个 Agent 开头声明 JOIN 或 SKIP，跳过还是参与让它自己判断，参与的导演以轮询方式各说一轮，最后 Critic 统一汇总。这套设计天然适合放在视频工作流最前面——用户给出的原始需求往往很粗，像&amp;quot;我不接受这个结局&amp;quot;&amp;quot;想让某个角色活下来&amp;quot;这种输入，离可执行的视频 prompt 还差很多层，多智能体讨论能在一开始就把需求转成更稳定的中间物：改写原则、情绪弧线、镜头重心、角色保留项、不能碰的设定。对 What-if Studio 这种意难平改写产品来说这一步尤其关键，因为要处理的远不只是生成一个爽感片段，还有原作立场、角色命运、观众心理补偿和影视表达技巧之间的平衡。&lt;/p&gt;
&lt;h3&gt;现有视频链路的局限&lt;/h3&gt;
&lt;p&gt;当前这版生成部分，翻一下 &lt;code&gt;video_pipeline.py&lt;/code&gt; 就知道，本质上还是把讨论结果压成 prompt，再调用视频模型。它能交付一个结果，也能让整条链闭环，但远没到理想状态——中间产物还很少，没有稳定的角色设定卡，没有场景圣经，没有镜头卡，没有关键帧缓存，也没按镜头去路由模型，脚本和生成之间是一条很粗的单通道，模型一旦漂移，系统几乎没有抓手去修。这正好把问题暴露得很清楚：下一代 What-if Studio 重点该补哪几层。&lt;/p&gt;
&lt;h2&gt;拆解&lt;/h2&gt;
&lt;p&gt;做完 What-if Studio 之后开始系统性地翻市面上其他视频生成产品怎么做工作流，越看越觉得：表面上大家都在做一句话成片，真正拉开差距的其实是背后有没有把工作流拆对。很多人刚接触视频生成时以为流程是剧本直接到视频模型，真正做起来才发现中间至少还有四层：资产、镜头、路由、质检。&lt;/p&gt;
&lt;h3&gt;需求不是 prompt&lt;/h3&gt;
&lt;p&gt;用户输入的&amp;quot;把这个意难平改成 HE&amp;quot;，说到底只是一个创意意图，不是执行指令。一个可执行的视频项目至少得先补上这些东西：改写目标（到底是抢救某个事件，还是重排整段叙事）、原作约束（哪些人物关系不能被写崩）、观众收益（是情绪补偿、人物救赎，还是世界观修补）、风格锚点（偏电影、偏漫剧、偏预告片，还是偏概念短片）、交付边界（是做 8 秒情绪镜头，还是 60 秒完整片段）。这一层更像 writers&apos; room，而不是模型调用。What-if Studio 现在的多导演讨论其实已经踏在这个位置上了，接下来最该强化的方向是让讨论结果沉淀成结构化约束，而不是让聊天更像人。&lt;/p&gt;
&lt;h3&gt;剧本要拆成分层资产&lt;/h3&gt;
&lt;p&gt;面向视频模型的剧本不能只有自然语言，真正能驱动生成链路的剧本至少得拆成三份：面向叙事的 beat sheet，讲清楚这一段情绪的推进和冲突节点；面向镜头的 storyboard 文本，讲清楚景别、机位、运动、调度、时长；面向生成的 shot card，讲清楚角色、场景、动作、起止状态、参考素材、模型偏好。很多平台型产品从外部看像一个超大 Agent，实际落地时往往会先把这几层拆开，用户看到的是一句话生成分镜，系统内部更合理的做法一定是先把一句话重写成多张镜头卡，再跑图片和视频模型。这也是为什么即梦这类 AI Agent 工作流看起来会先调大量图片 Agent——外部产品形态未必把内部实现完全公开，但从交互和产物推断，一个成熟的系统大概率会先补角色图、场景图、关键帧和分镜卡再进视频阶段，因为缺了这些中间资产，角色一致性、镜头衔接、局部返工几乎无从谈起。&lt;/p&gt;
&lt;h3&gt;角色一致性靠资产&lt;/h3&gt;
&lt;p&gt;只要开始做连续镜头，角色一致性就会变成第一道门槛。一句&amp;quot;黑发少女，冷静，穿校服&amp;quot;远远不够，系统真正需要的是一套角色圣经：稳定视觉 token（发型、脸型、瞳色、服饰层次、年龄感），禁止项（不要改变发色、不要忽然变成熟龄脸），三视图与多姿态参考，不同情绪下的表情边界，在不同光线和机位里的保真策略。这也是主流工作流里图片生成模型比预想中更重要的原因——视频模型不是孤立工作的，它前面经常会有一层图片资产生产链，专门负责生成角色三视图、场景设定图、关键帧、道具参考图，很多看上去是视频产品的系统，真正的生产量大头反而落在图片侧。对 What-if Studio 来说这一层几乎是下一步最该补齐的，因为产品核心是替你重拍一个你已经很熟的故事——越是用户熟悉的 IP，角色一致性越不能靠抽卡。&lt;/p&gt;
&lt;h3&gt;镜头卡才是调度单元&lt;/h3&gt;
&lt;p&gt;一个稳定的视频流水线不应该以整段剧本为最小执行单位，而应该以镜头卡为单位。理想的 shot card 至少要带这些字段：&lt;code&gt;shot_id&lt;/code&gt;、&lt;code&gt;purpose&lt;/code&gt;（这一镜头服务什么叙事目标）、&lt;code&gt;duration&lt;/code&gt;、&lt;code&gt;characters&lt;/code&gt;、&lt;code&gt;location&lt;/code&gt;、&lt;code&gt;action&lt;/code&gt;、&lt;code&gt;camera&lt;/code&gt;、&lt;code&gt;emotion&lt;/code&gt;、&lt;code&gt;start_frame_spec&lt;/code&gt;、&lt;code&gt;end_frame_spec&lt;/code&gt;、&lt;code&gt;reference_assets&lt;/code&gt;、&lt;code&gt;audio_notes&lt;/code&gt;、&lt;code&gt;model_preference&lt;/code&gt;、&lt;code&gt;retry_policy&lt;/code&gt;。工作流一旦切到镜头级，很多事就变得可工程化了——同一个项目里，开场大全景可以走偏氛围型，对话镜头可以走参考图更强的模型，过渡镜头可以走首尾帧控制，问题镜头可以单独重生成不用整段报废，镜头级缓存也更自然，前面已经锁好的角色资产和场景资产都能复用。&lt;/p&gt;
&lt;h3&gt;视频模型只是执行层&lt;/h3&gt;
&lt;p&gt;选模型常常被当成最核心的技术决策，但做到后面会发现它更像一层路由问题。当工作流足够完整时，模型的职责会收缩成一件事：根据镜头类型执行最合适的生成任务。系统需要的不是唯一的最强模型，而是一个会选模型的调度器。这个调度器至少要判断：这是纯文生视频还是图生视频、是单角色镜头还是多角色互动、需要的是高一致性还是高动态、需要首尾帧强约束还是给模型更大自由度、有没有可用的图片参考和视频参考和音频参考、失败后应该换提示词还是换参考资产还是直接切另一模型。从这个角度看，视频模型更像渲染后端，真正决定体验上限的是前面的资产组织能力。&lt;/p&gt;
&lt;h2&gt;能力&lt;/h2&gt;
&lt;p&gt;做完工作流拆解之后，系统过了一遍市面上几个主流视频生成模型的能力边界，搞清楚它们分别适合放在工作流的哪一层，哪些能力已经能确认、哪些还不能写得太死。&lt;/p&gt;
&lt;h3&gt;WAN&lt;/h3&gt;
&lt;p&gt;What-if Studio 现在就接的 Wan，对它相对熟一些。Wan 系列的优点在于能力边界清楚，而且很适合被工程系统调用。&lt;a href=&quot;https://github.com/Wan-Video/Wan2.2&quot;&gt;Wan 2.2&lt;/a&gt; 明确给出了文生视频 &lt;code&gt;T2V&lt;/code&gt;、图生视频 &lt;code&gt;I2V&lt;/code&gt;、文本加图像联合生成 &lt;code&gt;TI2V&lt;/code&gt;、语音驱动视频 &lt;code&gt;S2V&lt;/code&gt;，以及角色动画和替换相关能力。阿里云的万相参考生视频接口文档明确支持 &lt;code&gt;0～5&lt;/code&gt; 张图像、&lt;code&gt;0～3&lt;/code&gt; 段视频作为参考输入，图像和视频总数不超过 &lt;code&gt;5&lt;/code&gt;，参考接口还支持多角色引用和角色一致性标记，这对多人镜头非常关键。&lt;a href=&quot;https://developer.aliyun.com/article/1661415&quot;&gt;Wan2.1-FLF2V&lt;/a&gt; 这条线明确支持首帧加尾帧控制，用首尾两张图去生成中间过渡视频。几条能力结合起来看，Wan 很适合做约束比较强的镜头，以及需要把多角色参考资产吃进去的镜头。&lt;/p&gt;
&lt;h3&gt;Seedance&lt;/h3&gt;
&lt;p&gt;Seedance 2.0 支持文字、图片、音频、视频四种模态输入，支持全模态参考输入与编辑，明确强调原生音画同步，同时覆盖文生视频、图生视频、多模态生视频场景。这意味着它适合承担把多种上下文揉进一个镜头的任务，尤其是需要音画同步、参考素材比较丰富的场景。但关于很多创作者最关心的几个细节，官方页面并没有写得足够具体：没有在模型页明确写出首尾帧控制，没有明确写出视频续写或延长，也没有明确写出以前几秒视频作为条件继续往后生成。产品层面是否可能在上层工作流里提供类似能力是另一回事，在技术文章里最好将这两层分开——底层模型页确认的是全模态参考与音画同步，某些产品功能里出现的尾帧、补帧、拼装能力未必等于底层模型原生公开能力。这类边界区分对做平台的人很重要，设计工作流时不能把上层产品体验错当成底层 API 保证。&lt;/p&gt;
&lt;h3&gt;HappyHorse&lt;/h3&gt;
&lt;p&gt;HappyHorse 走的是另一种方向：把多模态参考、电影感和音视频同步整合进一个生成入口。从 &lt;a href=&quot;https://happy-horse.ai/zh&quot;&gt;HappyHorse&lt;/a&gt; 可确认的信息来看，它至少明确支持文生视频、图生视频、一个或多个参考图像、参考视频与音频参考等多模态输入（最多可组合到 &lt;code&gt;12&lt;/code&gt; 个输入），以及原生音视频同步、唇音同步、环境音效与多语言旁白类。如果某个镜头天然需要声音和画面一起成型，HappyHorse 这类模型会很有吸引力，它的接口思路更像把更多上下文一次喂进去。同样需要克制的是边界判断——至少从官方页面的公开表述里，目前还没有看到首尾帧控制、通用视频续写、以前几秒作为条件做稳定延展这几个点被明确写实，所以在工作流设计里更稳的做法是把它理解成强多模态输入、强音画一体的生成候选，而不是默认它能承担所有过渡控制任务。&lt;/p&gt;
&lt;h3&gt;模型分工&lt;/h3&gt;
&lt;p&gt;如果从 What-if Studio 的下一代形态出发，这几个模型应该放在不同位置，各管一段：&lt;/p&gt;
&lt;p&gt;| 位置                           | 更适合的模型 | 适合做什么                         |
| ------------------------------ | ------------ | ---------------------------------- |
| 角色与场景资产锁定后的视频执行 | WAN          | 图像参考、多角色参考、首尾帧过渡   |
| 多模态上下文压到单镜头         | Seedance     | 文本、图像、音频、视频一起参与生成 |
| 音画一体、电影感镜头           | HappyHorse   | 多模态输入、音视频同步、唇音联动   |&lt;/p&gt;
&lt;h2&gt;断层&lt;/h2&gt;
&lt;p&gt;回头看 What-if Studio 当前这一版的实现，它和成熟视频工作流之间的断层很具体。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;缺角色资产层&lt;/strong&gt;：讨论结果现在会收束成脚本，但还没有继续下钻成角色卡、场景卡、关键帧集合，视频模型吃到的上下文虽然比一句 prompt 多，却还是不够稳。如果要做 IP 改写，角色资产层基本不能省——没有它，系统只是在理解你的遗憾，还没有能力稳定地重拍它。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺镜头执行层&lt;/strong&gt;：当前链路已经有 &lt;code&gt;collect → analyze → discuss → edit → render → deliver&lt;/code&gt; 这条阶段骨架，但 &lt;code&gt;edit&lt;/code&gt; 到 &lt;code&gt;render&lt;/code&gt; 之间缺少真正的镜头级编排，系统知道自己在做视频，却不知道自己在做哪几个镜头。没有 shot card，失败就只能整段返工，模型路由也无从谈起。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺生成路由层&lt;/strong&gt;：目前的生成端更接近单模型直连，适合 demo，不适合平台化。一旦进入生产，很快会碰到这些问题：对话镜头要不要优先走参考图更强的模型、转场镜头要不要优先走首尾帧、纯氛围段落要不要放松约束换动态性、同一个项目里能不能一部分镜头走 Wan 一部分走 Seedance 或别的模型——这些都不是 prompt 工程问题，而是路由器问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺版本化中间产物&lt;/strong&gt;：创作系统和一次性生成器的最大区别在于中间产物能不能存下来。What-if Studio 下一步最值得补的，是把每个阶段的产物都显式化、版本化：改写原则 &lt;code&gt;rewrite_spec&lt;/code&gt;、角色圣经 &lt;code&gt;character_bible&lt;/code&gt;、场景圣经 &lt;code&gt;scene_bible&lt;/code&gt;、镜头卡 &lt;code&gt;shot_cards&lt;/code&gt;、关键帧包 &lt;code&gt;keyframe_pack&lt;/code&gt;、生成配置单 &lt;code&gt;generation_manifest&lt;/code&gt;、质检报告 &lt;code&gt;qc_report&lt;/code&gt;。只要这些东西存下来了，系统才能真的具备复拍、回滚、局部返工、迁移模型的能力。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;下一代&lt;/h2&gt;
&lt;p&gt;下一代 What-if Studio 的工作流大概可以拆成下面几层。&lt;/p&gt;
&lt;h3&gt;编排&lt;/h3&gt;
&lt;p&gt;多智能体的职责应该是把自由输入逐步压成结构化资产，而不只是聊天。当前导演会的体验可以保留，它非常适合意难平改写这个题材，但讨论结束后应该继续往下拆成多个专门 Agent：&lt;code&gt;Lore Guardian&lt;/code&gt; 负责原作边界和角色不崩，&lt;code&gt;Showrunner&lt;/code&gt; 负责故事总体取舍，&lt;code&gt;Script Planner&lt;/code&gt; 负责 beat sheet，&lt;code&gt;Shot Planner&lt;/code&gt; 负责镜头卡，&lt;code&gt;Character Bible Agent&lt;/code&gt; 负责人物视觉资产，&lt;code&gt;Scene Bible Agent&lt;/code&gt; 负责场景与道具资产，&lt;code&gt;Model Router&lt;/code&gt; 负责每个镜头选模型，&lt;code&gt;QC Agent&lt;/code&gt; 负责一致性、连续性、违和感检查。当前 demo 用 AutoGen 没问题，它很适合快速把多人讨论搭出来，下一步如果要把状态、工具调用、人工确认、重试分支、长流程恢复做得更扎实，可能需要考虑往 Microsoft Agent Framework 迁移。原因很朴素：What-if Studio 后面要处理的是一条跨多个工种、多个资产、多个外部模型的生产链，到那个阶段，显式状态机、可恢复执行、带类型的中间对象比大家轮流聊更重要。&lt;/p&gt;
&lt;h3&gt;数据&lt;/h3&gt;
&lt;p&gt;这一代最核心的升级是数据形态的转变。每个阶段都应该输出结构化对象，而不只是好看的自然语言，导演会结束之后不直接进视频生成，先落地一份中间规范：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;rewrite_goal&amp;quot;: &amp;quot;救下小天狼星，并保留大战后的代价感&amp;quot;,
  &amp;quot;non_negotiables&amp;quot;: [&amp;quot;哈利的成长不能被抹平&amp;quot;, &amp;quot;情绪基调不能变成纯喜剧&amp;quot;],
  &amp;quot;character_bibles&amp;quot;: [],
  &amp;quot;scene_bibles&amp;quot;: [],
  &amp;quot;shot_cards&amp;quot;: []
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结构化的意义不只在于方便程序读取，更在于方便人介入——用户可以在任意层级确认或打回，不用等到最终视频生成完毕才第一次发表意见。&lt;/p&gt;
&lt;h3&gt;资产&lt;/h3&gt;
&lt;p&gt;角色资产层会是下一代 What-if Studio 的分水岭。理想流程里，导演讨论完之后先做一轮图片资产生成，再进视频：角色三视图、角色情绪板、场景设定图、道具参考、每个关键镜头的首帧或关键帧。这一步可以完全 agent 化——一个 Agent 负责把角色描述转成标准视觉 token，一个 Agent 负责为不同镜头生成参考图，一个 Agent 负责筛掉不稳定结果，再把通过的资产挂回镜头卡。很多平台看起来像在直接生视频，真正稳定的部分往往都藏在这一层。&lt;/p&gt;
&lt;h3&gt;路由&lt;/h3&gt;
&lt;p&gt;下一代 What-if Studio 不应该绑定单一视频模型，而应该绑定一套镜头路由规则。按镜头类型来分：有强参考图和明确构图的镜头优先走 I2V，需要从 A 过渡到 B 的镜头优先走首尾帧控制，需要多模态上下文一起压进来的镜头优先走参考能力更强的模型，纯氛围镜头允许更大的自由生成空间，某个镜头失败之后先换参考资产、再换提示词、最后才换模型。这样做有两个直接好处：成本更可控，不是所有镜头都值得用同一种最贵能力；可修复，系统不需要整片重来，只需要知道这张镜头卡适合哪条重试路径。&lt;/p&gt;
&lt;h3&gt;交互&lt;/h3&gt;
&lt;p&gt;What-if Studio 最有价值的地方在于共创过程，用户交互节点应该做得更明确：导演会后确认改写方向，角色资产生成后确认人物是否像，镜头卡生成后确认叙事节奏，关键帧生成后确认视觉一致性，视频出来后只针对坏镜头局部返工。到这步，What-if Studio 就真正把用户拉进了影视工作流里，而不只是让他们作为旁观者观看 Agent 讨论。&lt;/p&gt;
&lt;h3&gt;工程&lt;/h3&gt;
&lt;p&gt;只谈创意不谈工程，视频工作流很快会失控。下一代至少要补这些基础设施：资产哈希与复用（避免重复抽同一角色图），镜头级缓存（单镜头失败不拖累全片），prompt 模板版本化（能比较哪版更稳），模型能力表（清楚记录每个模型能吃什么输入），成本账本（知道钱花在哪种镜头上），失败分流（区分素材问题、路由问题和模型问题）。这些基础设施不太显眼，但决定了 What-if Studio 能不能从 demo 走到产品。&lt;/p&gt;
&lt;h2&gt;落地&lt;/h2&gt;
&lt;h3&gt;导演会产物结构化&lt;/h3&gt;
&lt;p&gt;这是成本最低但收益最直接的一步。现在已经有多智能体讨论，下一步是让它稳定输出 &lt;code&gt;rewrite_spec&lt;/code&gt;、&lt;code&gt;shot_cards&lt;/code&gt;、&lt;code&gt;asset_requests&lt;/code&gt; 这类对象，只要这层做好，后续衔接图片 Agent、不同视频模型、人工确认节点都会顺很多。&lt;/p&gt;
&lt;h3&gt;补图片资产层&lt;/h3&gt;
&lt;p&gt;角色三视图、场景图、关键帧一旦进来，What-if Studio 才开始真正拥有重拍的能力，而不是重新抽一遍。这一步做完，系统对视频模型的依赖就会从完全依赖模型理解意图，变成由系统给出足够约束请模型执行，生成稳定性会立刻提一截。&lt;/p&gt;
&lt;h3&gt;镜头级模型路由&lt;/h3&gt;
&lt;p&gt;等前两层成立，再谈多模型协同才有意义，因为那时候系统已经积累了足够多的上下文，能区分某个镜头到底该走 Wan、该走偏全模态参考的路线、还是该走更强音画一体的路线。模型不再是全局开关，而是工位。&lt;/p&gt;
&lt;p&gt;做完 What-if Studio 这一个 demo，比较清楚的感受是：视频生成产品的真正壁垒不在于接了多少模型，而在于有没有把创意到约束到资产到镜头到生成到回修这条链做成一个可追踪、可插手、可复用的系统。当前 demo 版已经证明了改写意难平不是一句 prompt，而是一场可视化、可参与、可被编排的创作过程，接下来要做的就是让这场讨论真正接上工业化的视频工作流，让导演会不只产出观点，还产出能被下一环节稳定消费的资产——到了那一步，What-if Studio 才会从一个会说话的 demo，长成一个真正能拍片的 AI 剧组。&lt;/p&gt;</content:encoded></item><item><title>Multica：让 AI Agent 成为一等公民</title><link>https://saurlax.com/blog/multica/</link><guid isPermaLink="true">https://saurlax.com/blog/multica/</guid><description>AI 写代码的能力已不再是问题，现在的 Agent 可以读代码、写实现、跑测试、修 Bug，甚至直接开 PR。真正的问题在于：如何同时协调好多个 Agent？

单个 Agent 还能盯着，两个 Ag...</description><pubDate>Fri, 15 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;AI 写代码的能力已不再是问题，现在的 Agent 可以读代码、写实现、跑测试、修 Bug，甚至直接开 PR。真正的问题在于：如何同时协调好多个 Agent？&lt;/p&gt;
&lt;p&gt;单个 Agent 还能盯着，两个 Agent 还能手动协调，五个、十个便难以应对。任务拆分依赖记忆，进度靠切换终端，冲突靠肉眼发现。问题不在于工具本身不好，而是工具从一开始就不是为这个场景设计的。&lt;/p&gt;
&lt;h2&gt;当 Agent 多起来之后&lt;/h2&gt;
&lt;p&gt;给三个 Agent 分配了用户模块、订单模块和支付模块的开发任务。用户模块改了接口签名，订单模块的 Agent 并不知情，继续按旧接口在写。等到 review 时发现两边对不上，已经全是错的。&lt;/p&gt;
&lt;p&gt;这是必然会发生的局面。Agent 之间没有共享的上下文，每个都在自己的小世界里工作，人成了唯一的信息中转站。打开五个终端窗口各跑一个 Agent，时不时切过去扫一眼——Agent 多了之后，大部分时间不是在思考问题，而是在做监控。&lt;/p&gt;
&lt;p&gt;代码合并时问题更突出。编译器能发现语法错误，发现不了逻辑冲突。两个 Agent 各自实现了一套错误处理逻辑，调用约定不一致，只能靠人去读懂两边在做什么然后手动调和。Agent 越多，这种隐性集成成本越高。&lt;/p&gt;
&lt;p&gt;根源在于现有的项目管理工具默认执行者是人。Agent 在这些工具里只是一个名字，没有身份、没有状态、没有协作能力。&lt;/p&gt;
&lt;h2&gt;为什么现有产品不够用&lt;/h2&gt;
&lt;p&gt;GitHub Projects 很好用，但是面向人设计的。Issue 分配给用户，Project 看板追踪人的任务，Actions 响应人的操作，整个数据模型里没有 Agent 的位置。&lt;/p&gt;
&lt;p&gt;硬要把 Agent 塞进去，只能把它当成人来用——给 Agent 建个账号，让它在评论里汇报进度，手动把执行状态更新到 Issue 里。能跑，但别扭。别扭在哪里？Agent 调用了哪些工具、遇到什么错误、尝试了几种方案，全在它自己的会话里，GitHub 看不到。只能看到最终结果，出了问题没法复盘 Agent 是如何走到这一步的。&lt;/p&gt;
&lt;p&gt;Claude Code 里有 Agent Teams，能让多个 Agent 互相通信、认领任务。这比 GitHub Projects 进了一步，但会话断了一切都丢了——状态存在当前会话里，关掉 IDE 或者超时，谁在做什么、协调过什么，全没了。而且它假设的场景是一个人加 N 个 Agent，没有团队视图，没有权限管理，多个人协作管理 Agent 这件事根本不在设计范围内。&lt;/p&gt;
&lt;p&gt;更深层的问题是过程仍然是黑盒。Agent 之间的任务分配逻辑、决策路径、失败原因，没有结构化记录。想知道 Agent A 为什么放弃了方案 X 选择方案 Y，只能去翻会话日志。&lt;/p&gt;
&lt;h2&gt;Multica 做了什么&lt;/h2&gt;
&lt;p&gt;Multica 的思路很直接：把 Agent 当作真正的团队成员，不只是人的工具。&lt;/p&gt;
&lt;p&gt;听起来朴素，但意味着数据模型、交互方式、部署架构全部要重新考虑。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Agent 拥有独立身份。&lt;/strong&gt; 在 Multica 里，Agent 和人一样有自己的身份、能力描述、工作记录。Issue 可以分配给 Agent，Agent 可以认领 Issue，在 Issue 下面评论汇报进度。人和 Agent 可以在同一个工作流里协作，通过同样的机制沟通和追踪，Agent 产生的代码、文档、测试报告被当作工程资产沉淀下来，与人的产出同等对待。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Issue 记录的是过程，不只是待办。&lt;/strong&gt; 传统项目管理的 Issue 记录要做什么，Multica 的 Issue 记录这个过程里发生了什么——需求描述、讨论过程、决策理由、失败的尝试、上下文快照都在里面。Agent 执行任务时把思考过程、工具调用记录、错误信息都写进 Issue 或关联文档里。&lt;/p&gt;
&lt;p&gt;这样做的好处是 Agent 中断后可以从 Issue 里恢复现场，不只是接着上次的代码写，而是接着上次的思路想，知道之前试过什么、为什么放弃。这些记录还会沉淀为团队知识，下一个类似任务启动时，新的 Agent 可以读到上次的经验。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;实时掌握所有 Agent 的状态。&lt;/strong&gt; Multica 有一个通过 WebSocket 推送状态变化的实时面板，哪个 Agent 在执行、哪个遇到阻塞、哪个完成了工作，全部实时显示。每次状态变更和 Agent 行动都有日志，形成完整的可观测性链条。出了问题可以回溯，想优化流程可以分析数据。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;可以部署在内网。&lt;/strong&gt; Multica 是开源的，用 Go + PostgreSQL，可以部署在自己的环境里。架构上把 AI 能力和项目管理能力分开了——Multica 不管你用的是什么模型、什么 Agent 运行时，它只负责编排、追踪和沉淀。Agent 可以用 Claude Code，可以用 CodeBuddy Code，可以用内网部署的开源模型，Multica 管的是这些 Agent 之间的协作。&lt;/p&gt;
&lt;h2&gt;和 Agentic Engineering 放在一起用&lt;/h2&gt;
&lt;p&gt;Multica 本身是协作平台，和 Agentic Engineering 体系结合之后效果不只是叠加。&lt;/p&gt;
&lt;p&gt;Agentic Engineering 提供的是让 Agent 可靠写代码的方法论：结构化的上下文管理、分阶段交付、门禁机制、知识沉淀规范。Multica 提供的是让这套方法论在多 Agent 场景下可操作的基础设施。&lt;/p&gt;
&lt;p&gt;单 Agent 场景下，流程靠 Prompt 和规则约束。多 Agent 场景下，Multica 的 Issue 结构可以把流程固化下来：每个阶段是一个 Issue 或子 Issue，状态流转和门禁绑定，不通过评审就转不到下一阶段。&lt;/p&gt;
&lt;p&gt;知识沉淀上，Agentic Engineering 要求每次任务产生的经验写进 &lt;code&gt;context/&lt;/code&gt; 下的文档里。Multica 可以记录哪些上下文被哪些任务用了、效果如何，用数据来指导上下文管理的优化。可观测性上，Multica 记录的 Token 消耗、执行时长、失败原因、人工干预记录，为分析 Agent 为什么失败、哪个环节最容易出错提供了数据基础。&lt;/p&gt;
&lt;h2&gt;还不够好的地方&lt;/h2&gt;
&lt;p&gt;人仍然是瓶颈。任务从哪里来、怎么拆分、优先级怎么定、出现分歧谁拍板，现在还需要人参与。能同时管理的 Agent 数量，上限取决于人的响应速度，而非工具有多少功能。&lt;/p&gt;
&lt;p&gt;Agent 之间的协调还很初级，主要是事件驱动：一个 Agent 完成任务后通知其他相关方。两个 Agent 计划修改同一个文件，提前发现并协商，现在还做不到。&lt;/p&gt;
&lt;p&gt;质量审查需要分层。全靠人审查不够用，全靠自动化靠不住。现在在探索三层模型：自动检查管格式和基本错误，AI 辅助审查管代码质量和逻辑一致性，人来管架构决策和安全敏感的部分。&lt;/p&gt;
&lt;p&gt;任务拆分还不能完全自动化。把一个大需求拆成多个可并行执行的子任务，需要理解需求、代码结构和依赖关系，现在的模型能做，但不够可靠，需要更多项目上下文来辅助。&lt;/p&gt;
&lt;p&gt;成本也需要管理。并行意味着更多 Token 消耗，但不是所有任务都需要最强的模型。需要有依据地决定这个任务用什么模型、这个 Agent 跑多久、什么时候该人工介入而不是让 Agent 继续试。&lt;/p&gt;</content:encoded></item><item><title>北春南夏</title><link>https://saurlax.com/blog/north-spring-south-summer/</link><guid isPermaLink="true">https://saurlax.com/blog/north-spring-south-summer/</guid><description>![](image.jpg)

四月的尾声，深圳的空气里已经浮动着黏稠的湿热，与北方那个干爽、凛冽的春天截然不同。我在大连度过了将近三年的光阴，习惯了那里的风沙与萧瑟，如今回到南方实习，像是一株被移植...</description><pubDate>Thu, 30 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;image.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;四月的尾声，深圳的空气里已经浮动着黏稠的湿热，与北方那个干爽、凛冽的春天截然不同。我在大连度过了将近三年的光阴，习惯了那里的风沙与萧瑟，如今回到南方实习，像是一株被移植的植物，根系在陌生而又熟悉的水土里试探着寻找抓地力。&lt;/p&gt;
&lt;p&gt;趁着难得的闲隙，我出门踏青，试图在这座钢筋水泥的森林里，寻找一点关于季节交替的实感。&lt;/p&gt;
&lt;p&gt;走在公园的小径上，天色有些阴沉，云层压得很低，像极了大连初春时那种欲雨还休的午后。风里裹挟着泥土翻涌的腥气，那是暴雨来临前特有的躁动。四周的花草在闷热中显得有些无精打采，叶片上蒙着一层灰，正如那段日子里我心头蒙尘的期待。&lt;/p&gt;
&lt;p&gt;那时的空气是紧绷的，仿佛拉满的弓弦，随时可能断裂。我看着那些含苞待放却迟迟不肯绽开的花蕾，它们在气压的逼迫下焦灼地等待着。这种等待是无声的煎熬，像是一场独角戏，风在树梢间穿梭，发出沙沙的声响，像是在催促，又像是在嘲笑。我走在这样的天色下，胸口如同堵着一团湿棉花，呼吸都带着滞涩的沉重。周围的世界仿佛都静止了，只剩下我一个人的心跳，在沉闷的空气中被无限放大，那是某种即将决堤却又被死死压抑的轰鸣。&lt;/p&gt;
&lt;p&gt;终于，雨落了下来。&lt;/p&gt;
&lt;p&gt;起初是零星的几点，砸在脸上生疼，紧接着便是连绵的潮湿。我并没有撑伞，任由这漫天的雨丝将我与世界隔绝。雨声嘈杂，掩盖了所有的声响，也模糊了视线。在这灰蒙蒙的雨幕中，城市变得遥远而抽象，只剩下眼前被打湿的草木。&lt;/p&gt;
&lt;p&gt;雨水冲刷着叶片，也冲刷着我心底那些翻涌的尘埃。我没有奔跑，只是慢慢地走着，感受着那份湿冷渗透进衣衫。这是一种宣泄，也是一种冷却。所有的焦灼、所有的渴望，都在这淋漓的雨水中被稀释、被带走。雨水在地面汇聚成流，带走了落花，也带走了那些不切实际的幻想。那一刻，我感到一种近乎麻木的平静，仿佛所有的力气都随着雨水流走了，只剩下一个空荡荡的躯壳，在雨中独自站立，不再挣扎，也不再期盼天晴。&lt;/p&gt;
&lt;p&gt;不知过了多久，雨势渐歇，云层散去，久违的阳光穿透薄雾，洒在湿润的街道上。&lt;/p&gt;
&lt;p&gt;我抬起头，看见路边的花草经过雨水的洗礼，竟显出一种惊人的翠绿。那些曾经低垂的头颅，此刻挂着晶莹的水珠，在阳光下折射出剔透的光芒。空气变得清冽而通透，之前的闷热与压抑一扫而空，取而代之的是一种雨后特有的清新与开阔。&lt;/p&gt;
&lt;p&gt;我深吸了一口气，那是南方初夏的味道，热烈而自由。&lt;/p&gt;
&lt;p&gt;我忽然明白，那场雨已经停了。&lt;/p&gt;
&lt;p&gt;无论是大连的风沙，还是此刻深圳的骤雨，都已成为过去。我站在这片南国的土地上，看着眼前生机勃勃的绿意，心中那块曾经潮湿阴郁的角落，终于迎来了久违的干燥与温暖。那些曾经让我辗转反侧的情绪，如今看来，不过是过眼云烟，随着那场雨，彻底落定尘埃。&lt;/p&gt;
&lt;p&gt;我拍了拍衣角沾染的尘土，转身向出口走去。脚步轻盈，不再回头。&lt;/p&gt;</content:encoded></item></channel></rss>