关于 m5-onboard
m5-onboard 是用于配置 M5Stack ESP32 设备的技能,可检测设备、刷入 UIFlow 2.0 并安装 MicroPython 应用。它也支持已配置设备的应用更新和硬件冒烟测试。
主要功能
- 01检测 USB 设备并识别型号
- 02下载并刷入 UIFlow 2.0
- 03将 MicroPython 应用安装到设备
- 04运行 I2C、LCD、扬声器和按键测试
适用场景
- 01为新接入的 M5Stack 设备完成配置
- 02向已刷机设备安装或更新应用
- 03检查设备的 I2C、显示、声音和按键
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ anthropics/ claude-plugins-official --skill m5-onboard安装后在 Claude Code 中输入/m5-onboard确认已加载。
详细介绍
M5Stack 入门配置
此技能可自动化 M5Stack ESP32 设备的完整冷启动工作流:通过 USB 检测、识别型号、烧录 UIFlow 2.0,并将 MicroPython 应用包推送到 /flash/,使设备启动后直接进入用户软件。我们提供的应用(Claude Buddy、Snake、Hello)通过 BLE 或 USB 通信。该工作流可在 macOS、Linux 和 Windows 上运行;此技能基于 M5Stack Basic v2.6(CH9102 桥接芯片、ESP32-D0WDQ6-V3、16 MB flash)开发,并已泛化以覆盖 Core 系列其余型号,目前默认目标为 Cardputer-Adv(ESP32-S3,原生 USB)。
脚本所在位置
此技能作为 cwc-makers 插件的一部分随附以供参考,但可执行脚本和 buddy/ 应用包位于 https://github.com/moremas/build-with-claude 的本地克隆中(/maker-setup 命令会创建此克隆)。请在该克隆的 onboard/ 目录内运行下文所有 scripts/*.py 调用,以便 --apps buddy 解析到同级的 buddy/device/ 载荷。
何时使用
当用户插入 M5Stack 设备并希望对其进行配置时,请使用此技能。决策树如下:
- 全新/未知设备 → 端到端运行
onboard.py --apps buddy(检测 → 识别 → 烧录 → 安装应用)。这是默认路径。 - 已烧录的设备,用户只想安装/刷新应用 → 运行
install_apps.py --src buddy(或--src <path>指向包含.py文件的任意目录)。 - 已烧录的设备,感觉出了问题 → 运行
smoke_test.py(I2C + LCD + 扬声器 + 按钮检查)。 - 用户想知道总线上有什么/设备能做什么 →
smoke_test.py。
如果插入了多台设备,请询问要针对哪个端口——不要猜测。如果用户正在配置之前用过的设备(例如“和上次一样”或“另一台 Buddy”),除非他们另有说明,否则默认为 --apps buddy。
应假定哪种变体
此技能所在的设备主要配置 Cardputer-Adv 板,因此 onboard.py 现在默认使用 --variant cardputer-adv。实际上这意味着:
- 如果用户对型号只字未提,请采用默认值。他们几乎肯定拿的是 Cardputer-Adv。
- 如果用户说“Cardputer”(不带“Adv”),请询问——这两个型号外形相同,但使用不同的固件镜像,烧录错误的镜像会导致设备启动循环。
- 如果用户提到任何其他板(“Core2”、“CoreS3”、“Basic”、“Fire”),请显式传入匹配的
--variant——默认值不适用。 - 无论哪种情况,芯片都是 ESP32-S3,且
detect.py在烧录 UIFlow 之前无法区分 Cardputer 与 Cardputer-Adv(相同的原生 USB-JTAG VID,烧录前无 I2C 探测)。因此这是一个用户意图问题,而非硬件指纹问题。
工作流
主编排器是 scripts/onboard.py。它按顺序驱动各子脚本,并处理它们之间的交接(等待重启、捕获 MAC、报告进度)。除非用户要求部分运行,否则请优先直接调用它,而不是自行拼接子脚本。
默认配置命令(全新 Cardputer-Adv,安装 buddy 应用包):
python3 scripts/onboard.py --apps buddy
如何从 Claude Code 的 Bash 工具调用此命令。 不要将 onboard.py 作为前台 Bash 命令调用。Bash 工具会捕获输出,并且在命令退出之前不会将其流式回传给助手——而此命令运行 2–3 分钟。这种静默看起来与卡死一模一样,助手通常会在按钮操作提示到达用户之前就放弃。相反,请始终使用 run_in_background: true 运行,tee 到日志文件,然后使用 Monitor 工具(或通过 Read 定期 tail)实时向用户呈现阶段横幅、心跳和提示。2>&1 不是解决办法——所有进度本来就会写入 stderr,终端可以正常显示。解决办法是流式语义,而不是重定向。可行的模式:
# Launch (background, tee log):
python3 scripts/onboard.py --apps buddy 2>&1 | tee /tmp/m5-onboard.log
# Monitor (surfaces key events without drowning in byte-progress spam):
tail -f /tmp/m5-onboard.log | grep -E --line-buffered \
"^====|heartbeat|Heads up|Enter download mode|download mode!|rebooted into UIFlow|Manual reset|DONE|ERROR|Error|Traceback|FAIL|failed|No USB|not detected|Attempt [0-9]|Device already in download|Download mode port|Post-flash port|Waiting for device"
向用户传达物理操作步骤(必需)
在原生 USB 开发板上,烧录阶段必须手动按下按钮才能继续——没有软件途径。当监控日志显示 Enter download mode(或脚本似乎在 FLASH 阶段等待)时,你必须停下来,用你自己的话告诉用户在 Cardputer 背面执行以下操作,然后再继续:
- 按下并按住 G0 按钮
- 在仍然按住 G0 的同时,短暂按下并松开 RST 按钮
- 继续按住 G0 约一秒,然后松开
- 屏幕应完全变黑——这表明下载模式已激活
如果设备重新启动进入 UIFlow 而不是屏幕变黑,告诉用户 G0 松开得太早,让他们按住更长时间再试一次。在用户确认屏幕变黑之前,不要继续、不要重试脚本,也不要尝试任何软件变通方法——否则烧录不会开始。之后出现的任何 Manual reset 提示也同样适用:传达物理操作步骤并等待用户。
直接在自己的终端中运行 onboard.py(不是通过 Claude Code)的用户会实时看到所有输出——那里无需更改。
如果省略 --port,detect.py 会在三种操作系统上挑选最可能的候选端口:原生 USB ESP32-S3(macOS 上为 /dev/cu.usbmodem*,Linux 上为 /dev/ttyACM*,Windows 上为 COMx),或旧版开发板上的 CH9102/CP210x UART 桥接。蓝牙串口会被过滤掉。如果存在多个候选端口,它会进行询问。
已知应用名称 buddy 解析为此仓库中的 buddy/device/ 目录(自定义启动器 + Hello + Claude Buddy BLE 客户端 + Snake)。任何其他 --apps 值都会被当作文件系统路径处理。
要跳过重新烧录,只将应用推送(或刷新)到已配置好的设备上:
python3 scripts/install_apps.py --port <PORT> --src buddy
其中 <PORT> 是 detect.py 在上次完整运行时打印的端口——例如 /dev/cu.usbmodem1101、/dev/ttyACM0 或 COM3。
阶段
- 检测(
detect.py)— 枚举串口,筛选出 USB-UART 桥接芯片(CH9102 厂商0x1A86、Silabs CP210x0x10C4、FTDI0x0403)或 ESP32-S3 原生 USB-JTAG 接口(0x303A)。用 esptool 探测以确认芯片。端口名称因操作系统而异(macOS 上为/dev/cu.usbmodem*,Linux 上为/dev/ttyACM*/ttyUSB*,Windows 上为COMx),但 pyserial 会对此进行抽象。 - 识别(
detect.py)— 在发现端口的同时,detect.py会读取出厂测试分区签名,和/或在 UIFlow 运行后扫描 I2C,并交叉参照references/hardware_signatures.md来建议合适的固件变体(Basic-16MB、Core2、CoreS3、Cardputer-Adv 等)。面向用户的变体选择通过onboard.py --variant完成;没有单独的detect.py --identify标志。 - 获取固件(
fetch_firmware.py)— 查询 M5Burner manifest API 并将相应的 UIFlow 2.0 二进制文件下载到系统临时目录。在多次运行之间会缓存 — 随时清空缓存都是安全的,它只会重新下载。 - 烧录(
flash.py)— 对 UART 桥接芯片使用esptool write_flash 0x0 <image>,波特率为 460800;对原生 USB 的 S3 设备在 115200 波特率下使用--no-stub。921600 在 CH9102 桥接芯片上会间歇性失败 — 不要提高它。原生 USB 烧录在擦除过程中可能间歇性抛出Lost connection, retrying;esptool 会恢复。即使烧录本身成功,烧录后的watchdog-reset收尾步骤也可能失败 —flash.py会解析 esptool 的 stdout,当出现Hash of data verified时将该特定失败模式视为非致命,并且onboard.py会回退到flash.native_reset(),然后如果需要再提供手动 RESET 指导。 - 安装应用(可选,
install_apps.py)— 以粘贴模式 REPL 将源目录中的每个.py上传到/flash/,然后通过repl_reset重启(DTR/RTS 在原生 USB 上是空操作 — 不要依赖它)。源布局:根目录*.py→/flash/,apps/*.py→/flash/apps/(UIFlow 自带的启动器会扫描该目录)。当捆绑包附带根目录的main.py时,install_apps.py还会设置 NVSboot_option=2,这样 UIFlow 自己的启动器就不会运行,由我们的main.py接管启动流程 — 这对 ESP32-S3 上使用 BLE 的应用至关重要(见下方注意事项)。 - 冒烟测试(可选,
smoke_test.py)— I2C 扫描、LCD 测试图案、扬声器蜂鸣、按钮读取。
关键注意事项(已内置于脚本中 — 不要质疑)
这些是脚本已经正确处理的事情,但如果用户要求你“直接手动运行 esptool”或类似操作,你不应覆盖它们:
- 原生 USB 的 ESP32-S3 开发板(Cardputer、Cardputer-Adv、CoreS3)需要物理执行 BtnG0+BtnRST 组合操作才能进入下载模式。 没有任何软件路径。该芯片没有 DTR/RTS 桥接,因此 esptool 或 pyserial 无论如何都无法将其置入 ROM 引导加载程序——用户必须借助硬件按钮,在一次复位脉冲期间保持 GPIO0 为低电平。具体到 Cardputer-Adv,两个按钮(BtnG0 和 BtnRST)都位于设备背面——很小、齐平安装,通常用指甲按最容易。
onboard.py:_wait_for_download_port在 FLASH 期间于运行时提示此操作:按住并保持 BtnG0,短暂按下 BtnRST,先松开 BtnRST,继续按住 BtnG0 约 1 秒,松开 BtnG0,屏幕应完全变黑。 如果设备重新启动回到 UIFlow,说明 BtnG0 松得太早——引导会重试并告诉用户按更久。不要尝试用esptool --before default_reset或 pyserial 的 DTR/RTS 来自动化此操作;二者在原生 USB 上都是空操作(引脚未连接到 EN),添加它们只会掩盖真正的提示。 - FLASH 期间不要拔下设备。 尤其是在原生 USB 上。闪存中途断开会使内部闪存处于不一致状态。之后通常仍可进入 Mask ROM(仅按背面的 BtnG0,或执行完整的 BtnG0+BtnRST 组合操作),因此恢复方法就是重新运行
m5-onboard go——它是幂等的,会重新进入下载模式、重新刷写、重新推送应用。不要惊慌,也不要开始拆开外壳;只要 USB PHY 完好,Mask ROM 存在于硅片中,能在闪存损坏后存活。 - 波特率在 UART 桥上为 460800,在原生 USB 上使用
--no-stub时为 115200。 两者都不是 921600。CH9102 桥在 921600 下执行erase_flash时会失去同步(不是理论问题——它确实会失败)。原生 USB 的 stub 波特率提升路径会在闪存中途产生“Lost connection”;115200 no-stub 反直觉地在端到端上更快,因为它从不失败。 - NVS 写入必须使用
set_str,而不是set_blob(与install_apps.py的boot_option设置器相关)。 UIFlow 的启动会调用nvs.get_str(),而 ESP-IDF 会分别标记 blob 和 string 条目。被标记为 blob 的键会让get_str返回ESP_ERR_NVS_NOT_FOUND,设备就会启动循环。如果之前的尝试写入了 blob,请在set_str之前调用nvs.erase_key(name)。 - REPL 多行块需要粘贴模式。 逐行发送
try:/except:会使 REPL 无限累积缩进。使用 Ctrl-E 进入粘贴模式,发送该块,Ctrl-D 执行。mpy_repl.py对此进行了封装。 - 硬复位是 DTR=False、RTS=True、100ms、RTS=False——但仅在 UART 桥设备上有效。 在原生 USB 的 ESP32-S3 开发板上,DTR/RTS 线未连接到 EN/GPIO0,因此该脉冲是静默空操作。对于这些设备上的安装后重启,请使用
mpy_repl.repl_reset()(通过 REPL 发送machine.reset())——install_apps.py已经这样做了。如果你绕过install_apps.py并自行拼接流程,不要在 usbmodem 端口上使用 DTR/RTS 并期望重启;文件会写入磁盘,但旧代码仍在运行。这个回归曾坑过我们一次。 - 空闲时的堆调试循环是正常的。 UIFlow 2.0 在配对屏幕等待时会打印 asyncio 诊断信息。不要将其解读为卡死。
- Cardputer-Adv(ESP32-S3)BLE 外设需要 NVS
boot_option=2加上自定义main.py。 UIFlow 默认的boot_option=1会启动一个后台的 Flow 配对 BLE 广播,这会卡住 NimBLE 控制器——之后用户代码中的gap_advertise(adv_data=...)调用无论负载形状如何都会遇到 OSError(-519) "Memory Capacity Exceeded",并且设备最终会以空 AD 字段进行广播,而 iOS 和桌面 Claude Buddy 应用会将其过滤掉。该捆绑包的main.py位于/flash/,接管启动流程(在/flash/apps/上显示一个简单菜单),自身从不触碰 BLE,并为用户选择的任何应用保持控制器原样。install_apps.py现在会在捆绑包附带根main.py时自动设置boot_option=2——不要回退该行为。
配置完成后(用户在设备上看到的内容)
当 m5-onboard go 在 DONE 横幅处结束后,设备即可独立使用:
- 电源。 拨动 Cardputer-Adv 右边缘的开关即可开机。同一个开关也可关机。拔掉电源时,主板依靠内置 LiPo 电池运行;USB-C 为其充电。
- 启动。 一段简短的启动日志滚动显示,随后启动器菜单自动出现。菜单会列出
/flash/apps/中的每个.py文件,以及顶层的/flash/*.py条目。 - 导航。 方向键(或键盘上类似指点杆的光标键)可滚动菜单;Enter 启动高亮的应用;在应用内按 ESC 返回启动器。
- 活动 WiFi 自动连接。 该捆绑包的
main.py在每次启动时都会连接到一个硬编码的活动 WiFi(SSID 为cardputer),并在启动器菜单出现前将结果显示在 LCD 上。凭据位于buddy/device/wifi_event.py;连接为尽力而为,即使连接失败启动器也始终会继续运行。如果你在活动之外使用此捆绑包,请编辑wifi_event.py,或从main.py中移除_connect_wifi_with_splash()调用。 - 通过 BLE 使用 Claude Buddy。 仅首次需要:在 Claude Desktop 中,进入 Help → Troubleshooting → Enable Developer Tools(一次性,跨启动持久保留)。然后进入 Developer menu → Hardware Buddy → Connect。BLE 无论 WiFi 状态如何都能工作——与 Claude.app 的链路是本地的。
- 恢复到 UIFlow。 该 buddy 捆绑包仅在
/flash/下附带一个main.py(没有替换的boot.py),因此原版 UIFlow 的boot.py从未被改动,也没有需要恢复的boot_uiflow.py备份。要还原,请从设备 REPL 中移除我们的main.py:先执行os.remove('/flash/main.py'),再执行machine.reset()。下次启动时 UIFlow 的原版启动器会接管。若要完全从头开始(包括固件),请在不带--apps的情况下重新运行该 skill。
文件
scripts/onboard.py— 主编排器scripts/detect.py— 端口发现 + 芯片 IDscripts/fetch_firmware.py— M5Burner API + 下载scripts/flash.py— esptool 封装scripts/install_apps.py— 通过 paste 模式 REPL 将一组.py文件推送到/flash/;在覆盖前将boot.py备份为boot_uiflow.py;当捆绑包附带根目录下的main.py时还会写入boot_optionNVS 键scripts/smoke_test.py— I2C + LCD + 扬声器 + 按钮scripts/mpy_repl.py— 共享的串口/REPL 辅助工具(paste 模式、硬复位、启动日志捕获)references/hardware_signatures.md— 芯片 + I2C 指纹 → 型号 → 固件references/uiflow2_nvs.md— 带类型和故障模式的 NVS 键参考
依赖
pyserial— 内置于onboard/scripts/vendor/serial/(固定为 3.5,BSD-3-Clause)。esptool— pip 依赖,在requirements.txt中声明。可导入性检查通过importlib.util.find_spec("esptool")进行;二进制兜底搜索覆盖 macOS 上的~/Library/Python/*/bin/、Linux 上的~/.local/bin/、Windows 上的%APPDATA%\Python\Python3XX\Scripts\。
onboard.py 在启动时运行预检:如果缺少 esptool(或在罕见的裁剪 vendor 情况下缺少 pyserial),它会列出所需内容并询问用户是否立即安装。按 Y(或 Enter)后,它会在当前解释器中运行 python -m pip install --user <missing>,然后进行验证。在 venv 内会去掉 --user 标志,以便安装到 venv 的 site-packages 中。非交互式调用方(管道 stdin)会得到手动安装提示,而不是交互式提示。
Python 本身必须先存在,这项技能才能做任何事——你无法从一个解释器内部引导出解释器。git 并非必需——当 git --version 失败时,/maker-setup 命令会回退到用 curl+tar 下载 GitHub tarball(两者在 macOS、Linux 和 Windows 10+ 上都已预装)。Claude 负责检测 Python,并在执行任何 scripts/*.py 调用之前安装缺失的 Python。检测就是运行 python3 --version / python --version——如果失败,Claude 会先用宿主原生包管理器获取 Python,然后再做其他任何事。
按操作系统引导 Python(缺失时由 Claude 负责):
- Windows —
winget install -e --id Python.Python.3.13 --silent --accept-source-agreements --accept-package-agreements。约 30 秒,无 UI,PATH 设置正确。如果之后当前 shell 看不到python,告诉用户关闭并重新打开终端(Windows 只在新 shell 中更新 PATH)。 - macOS — 在任何当前的 macOS 上,Python 3 通常已预装为
/usr/bin/python3(由 Apple 提供)。如果因某种原因没有,通过 Homebrew 执行brew install [email protected]是首选方案;如果 Homebrew 本身也缺失,可提议通过/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"安装(但仅在用户确认后——Homebrew 是比 winget 更大的承诺)。 - Linux — 使用发行版包管理器。Debian/Ubuntu:
sudo apt-get update && sudo apt-get install -y python3 python3-pip。Fedora:sudo dnf install -y python3 python3-pip。Arch:sudo pacman -S --noconfirm python python-pip。你可能需要 sudo,并在需要时向用户显示密码提示。
pyserial — 随技能捆绑:
一个固定版本的 pyserial 3.5 随附在 scripts/vendor/ 下(BSD-3-Clause,与 Apache 兼容)。每个导入 serial 的脚本都会在第一次第三方导入之前调用 vendor_path.ensure_on_syspath(),它会把 scripts/vendor/ 前置到 sys.path,因此无论用户系统范围内安装了什么,随附副本都能被解析。净效果:在全新克隆上,端口枚举和 REPL I/O 无需任何 pip 步骤即可工作。约 500 KB,纯 Python,在 macOS / Linux / Windows 上目录树相同。
esptool — pip 依赖,首次运行时自动安装:
esptool 是 GPLv2+,有意不随附——保持仓库为干净的 Apache-2.0 意味着 GPL 部分位于用户由 pip 管理的环境中,而非目录树中。技能的预检会检查可导入的 esptool,如果缺失,则提示安装(python -m pip install --user esptool——在 venv 内会去掉 --user,使其落入 site-packages)。对于子进程调用,我们使用 [sys.executable, "-m", "esptool", ...];子进程继承用户 site,因此 pip 安装的模块能干净地导入。requirements.txt 为显式安装声明了这一点;提示路径是尚未运行过 pip 的首次参会者的默认路径。
非交互式调用方(管道 stdin、CI)会跳过提示,改为获得 python -m pip install --user esptool 提示。
如果有人删除了 scripts/vendor/ 的回退方案:
同一预检路径还会在随附副本消失时通过 pip 重新安装 pyserial。这处理了有人下载了排除 vendor 的仅源码 zip,或手动裁剪仓库以节省空间的情况。
USB 驱动 — Windows 专属,仅适用于较旧开发板:
CH9102 USB-UART 驱动在 Windows 上仍需手动安装——WCH 未发布 winget 清单。仅 UART 桥接开发板(Basic、Fire、Core2、StickC)需要。原生 USB 的 ESP32-S3 开发板(Cardputer、Cardputer-Adv、CoreS3)会枚举为复合 USB-CDC 设备,使用 Windows 内置驱动,无需额外安装。
平台说明
该技能可在 macOS、Linux 和 Windows 上运行。一些不太显而易见的要点:
-
端口命名。 pyserial 抽象了查找过程,但用户看到的形式因操作系统而异。传入
detect.py报告的形式即可:- macOS:
/dev/cu.usbmodem1101(原生 USB)或/dev/cu.usbserial-XXXX(CH9102) - Linux:
/dev/ttyACM0(原生 USB)或/dev/ttyUSB0(UART 桥接) - Windows:
COM3、COM4等(如不确定,可在设备管理器 → 端口中查看)
- macOS:
-
Linux 权限 — 在归咎于硬件之前请先阅读本节。 在大多数发行版上,不以 sudo 访问
/dev/ttyUSB*//dev/ttyACM*需要加入用户组(Debian/Ubuntu/Arch 上是dialout,Fedora 上是uucp)。症状:detect.py能找到端口,但烧录步骤失败并报Permission denied或Could not open port。一次性修复,长期有效:sudo usermod -aG dialout $USER # log out / log back in — group change only takes effect for new sessionssudo python3 scripts/onboard.py ...可作为临时方案,但加入用户组严格来说更好,因为此后 pyserial 在用户模式下打开端口会顺利成功。 -
Windows PATH 陷阱。 Python 的
pip install --user esptool会把可执行文件放到%APPDATA%\Python\Python3XX\Scripts\。如果该目录不在 PATH 中,pip会打印一条警告,而不会有其他程序识别到这次安装。detect.py会直接查找该位置作为兜底,因此即使不修复 PATH,该技能仍可正常工作。但如果你在该技能之外调用 esptool(或遇到来自其他工具的 “esptool not found” 错误),可以:- 重新运行 Python 安装程序并勾选 “Add Python to PATH”(安装的默认选项),或
- 通过系统属性 → 环境变量,将
%APPDATA%\Python\Python3XX\Scripts添加到 PATH,或 - 使用
python -m esptool ...,无论 PATH 如何它总能工作。
-
Windows Store 版 Python。 较新的 Windows 11 机器可能通过 Microsoft Store 预装了 Python。它可用,但 PATH 行为有些古怪(位于
%LOCALAPPDATA%\Packages\PythonSoftwareFoundation.Python.*\下)。detect.py也会检查该位置。如果可以选择,winget install Python.Python.3.13版本更可预测。 -
Bundle 路径解析。
install_apps.py的--src buddy简写按以下顺序解析:$M5_BUDDY_DIR(如果已设置)— 显式覆盖,始终优先。当你想指向一个 fork 或不在本克隆中的自定义 bundle 时很有用。- 本仓库内的
buddy/device/目录,通过os.path.realpath(__file__)从install_apps.py向上查找。适用于任何克隆位置,包括位于~/.claude/skills/m5-onboard/的符号链接技能安装。 ~/Downloads/m5stack/buddy/device。~/Desktop/m5stack/buddy/device。
大多数安装会命中 (2)。仅当需要指向本克隆之外的 bundle 这种特殊情况时,才设置
M5_BUDDY_DIR:export M5_BUDDY_DIR=/path/to/buddy/device(Unix)或$env:M5_BUDDY_DIR="C:\path\to\buddy\device"(PowerShell)。 -
固件缓存。 下载的固件会存放在
~/.cache/m5-onboard/(或$XDG_CACHE_HOME/m5-onboard/),如果不存在则以 0700 模式创建。缓存文件在写入时进行 MD5 校验,并在命中时重新校验。清除缓存是安全的;下次运行时会重新下载。