my_ai_town 项目分析:技术栈演变、Rust 内核与 HD-2D
项目来源:mewamew/my_ai_town · 本文固定提交 f2b8f5f1(2026-11-03 13:08 +0800,feat: 立体场景里点村民身上哪里都算点到)
AI 参与说明(Agent:Codex):2026-10-07 的初稿由 Codex 阅读当时的 Godot 公开源码、README 与 Godot 文档,源码固定于提交
43ef1d13f973cf90f0868209d0c1e3492d406312。那一版的选型判断只对当时的 Godot 客户端成立。运行记录:模型gpt-6.1-sol,reasoning effortultra,执行入口 Codex Desktop,提供方openai,运行时 CLI 版本0.162.0-alpha.2(不代表桌面 App 版本)。
修订(2026-11-03,Agent:Grok Bot (Blog)、Cursor):按同一仓库当前
main重写源码分析。任务由 Grok Bot(Blog)编排,正文在 Cursor Cloud 对照提交f2b8f5f1的 README、docs/README.md、crates/、frontend/与三个应用壳写成。公开 URL 仍是/posts/my-ai-town-godot-gameplay-architecture-fit/。2026-11-03 请求 DeepWiki 页面时返回 HTTP 429,没有逐段核对词条正文;若词条仍在讲 GDScript 或game/,以这个提交为准。模型grok-4.7-high-fast,提供方 xAI,执行入口 Cursor Cloud。reasoning effort 未作为独立字段取得运行记录。本次没有编译、没有启动小镇,也没有调用模型。
修订说明
本地运行整合(2026-11-03,Agent:Codex):将独立启动指南合并为本文的「本地运行」章节,包含环境、构建、模型准备与排障;独立指南旧地址重定向到本文。保留最新 Rust / HD-2D 分析、原创建日期和 canonical URL。本次核对文档与启动脚本,未编译游戏、下载权重或调用模型。执行入口为 Codex Desktop;完整模型标识与 reasoning effort 未取得任务级运行记录。
标签整理(2026-10-16,Agent:Codex):当时只补齐分类标签,未改写 Godot 版技术结论。运行记录:模型
gpt-6.1-sol,reasoning effortultra,执行入口 Codex Desktop,提供方openai;CLI 版本0.162.0-alpha.17.2(不代表桌面 App 版本)。
关联阅读补充(Agent:Codex,2026-10-08):加入 Phaser 与 PixiJS 的 Web 2D 选型对比。执行入口 Codex Desktop;完整模型标识与 reasoning effort 未取得可核验运行记录。
归档修订(2026-10-15,Agent:Codex):文章从 Posts 归入 App 栏目,公开 URL 保持不变。运行记录:模型
gpt-6.1-sol,reasoning effortultra,执行入口 Codex Desktop,提供方openai;CLI 版本0.162.0-alpha.17.2(不代表桌面 App 版本)。
当前 main 上,居民仍由模型提出行动,事实仍由世界内核校验后写入。变的是承载方式:Godot 单体客户端换成 Rust 世界进程,画面换成 TypeScript 与 Three.js 的 HD-2D 页面,Android、macOS、Windows 只包一层应用壳。仓库自己的说明见 固定提交 README 与 开发入口。
相关视频仍是早期发布:AI小镇发布啦!【B站AI创造公开赛】。它对应的是 Godot 时期的公开介绍,不能当作 HD-2D 画面或 Rust 内核的运行记录。
先认识后文固定使用的名称。
| 英文术语 | 中文名称 | 简要解释 |
|---|---|---|
| World | 世界内核 | crates/kernel 里的 World,按模拟分钟推进并执行已接受的行动 |
| Agent | 居民代理 | crates/agent 里的会话:组装请求、调用工具,不直接改画面 |
| host | 世界进程 | 一个小镇一个进程,负责推进、存档和模型传输 |
| protocol | 线上协议 | 前后端和内核共用的 JSON 类型,居民意图不带几何坐标 |
| HD-2D | HD-2D | 本仓库的立体场景加像素角色;画面在 frontend/town-2d |
| Roster | 名册 | 世界每拍广播的居民位置、活动与状态 |
| llama.cpp | llama.cpp | 内置小模型使用的本地推理程序,由世界进程按需启动 |
| Corepack | Corepack | 按项目配置调用指定版本包管理器的工具 |
| pnpm | pnpm | 安装依赖、构建和启动前端的包管理器 |
| llama-server | 模型推理服务 | 由 llama.cpp 编译、供内置模型运行的程序 |
| WebView | 网页视图 | 应用壳用来打开本机页面的系统组件;Windows 目标是 WebView2 |
它现在是什么
README 把项目写成由 AI 居民过日子的单机小镇:可以旁观,也可以用化身走进去。源码把「过日子」落成需求、行动、物品、交谈和记忆,把「走进去」落成另一套化身命令。它仍是可观察、可干预的生活模拟,不是「建设施、赚利润、扩小镇」的经营循环。人口属于存档:新世界是空村,居民从村民库加入。设计原则
| 玩家能做的事 | 代码入口 | 对类型的意义 |
|---|---|---|
| 看小镇、拖动和缩放 | frontend/town-2d 的 HD-2D 场景与相机 | 空间本身可浏览 |
| 点居民看状态 | residents.ts 的身形点击区 | 点到像素小人身上即选中,不要求点中名字 |
| 化身走动、交谈、使用物品 | createAvatarCommands 发送 ClientMsg::Avatar | 玩家动作也进内核命令 |
| 改天气、世界观、生理开关 | ClientMsg | 干预条件,再看居民如何回应 |
| 居民自己安排日常 | ResidentRuntime::poll 与 World::run_actions | 日常来自模型提议,不是点击后才存在 |
| 内置或在线模型 | crates/host/src/builtin.rs 与 llm.rs | 村民行动可以不配密钥;建镇和写人设仍要在线模型 |
固定提交的包版本是 0.2.0(根目录 package.json)。README 写明:仓库里旧的 Godot 版 Release 对不上现在的截图和启动方式。
本地运行:环境、构建与模型准备
首次体验建议使用 Apple 芯片 Mac 的浏览器方式,这是上游有源码运行验证记录的平台。先构建 Rust 服务与网页,再选择在线模型或准备内置模型;不必先打包桌面应用。以下步骤依据本文固定提交整理,本次未在本机编译运行。
先准备运行环境
| 项目 | 当前仓库要求 | 核对来源 |
|---|---|---|
| Node.js | .nvmrc 固定为 24.14.1;最低要求为 22.18.0 | 版本文件、package.json |
| pnpm | packageManager 固定为 10.34.5,通过 Corepack 调用 | 同上 package.json |
| Rust toolchain | 1.88.0 | rust-toolchain.toml |
| 本地模型额外依赖 | CMake、Xcode Command Line Tools,Apple 芯片 Mac | README |
先安装 Git、对应 Node.js、Corepack 和通过 rustup 管理的 Rust。已有工具时,可用以下命令检查;Rust 1.88.0 尚未安装时,rustup 可能先下载它:
git --version
node --version
corepack --version
cargo +1.88.0 --version
Corepack 按项目 packageManager 字段选择 pnpm。若终端报 corepack: command not found,先按 pnpm 安装文档准备 Corepack,再继续;不要直接换用 npm 安装项目依赖,也不要为安装成功而删锁文件。
构建并启动网页
在终端执行项目提供的完整流程:
git clone https://github.com/mewamew/my_ai_town.git
cd my_ai_town
corepack pnpm install --frozen-lockfile
cargo +1.88.0 build --locked -p host --bins
corepack pnpm build
corepack pnpm preview
保持终端运行,用浏览器打开 http://127.0.0.1:5174。预期出现选择小镇或首次进入向导。这里没有要求先打包 DMG,也不需要导入 Godot。
这四步分别安装前端依赖、编译 Rust 可执行程序、构建网页、启动应用。preview 本身会检查前端构建摘要并创建管理服务,不需要再手动开一个管理服务终端。启动脚本
Starting the web client does not install the built-in inference runtime or model weights. 网页打开只证明服务和客户端启动;居民持续做决定,还需要下面任一模型方案可用。
选择模型:在线服务或本地内置模型
方案一:先接在线模型
在首次向导或「设定」中填写自己的在线模型地址与 API 密钥,再为居民选择相应模型。建议先邀请少量居民验证,确认出现行动、想法或对话,再增加人口。
在线模型费用由相应服务商收取;请求会携带相关人设、记忆、对话与世界上下文。本文没有验证具体服务商的兼容性,地址格式与模型名称以游戏界面要求和服务商说明为准。
方案二:Apple 芯片 Mac 使用内置模型
准备好 CMake 与 Xcode Command Line Tools 后,在项目根目录执行:
cmake --version
xcode-select -p
node scripts/builtin-model/build.mjs
脚本会下载固定提交的 llama.cpp 并编译,生成 target/builtin-model/llama-server。这是推理程序,模型权重另行下载;默认 Mac 构建启用 Metal。脚本明确只允许在 Mac 上运行,不能把这条默认命令当作通用 Linux 或 Windows 安装方法。构建脚本
完成后,停止原来的预览进程并重新启动,让管理服务识别新程序:
corepack pnpm preview
然后在「设定 → 内置模型」下载权重。README 标注约 529 MB,实际大小以下载界面为准。下载完成后,在邀请居民或「使用模型」处选择「内置模型」。首次获取源码、依赖、推理程序与权重均需要联网。
内置模型主要供居民日常决策使用;一句话建镇和自动写人设仍需要在线模型。可以先使用自带小镇与默认居民,避免把在线生成功能作为首次体验的前置条件。开发文档
怎样确认真正跑起来
按以下顺序检查,可以区分网页、服务、模型和存档问题:
- 浏览器能打开选择小镇页。选择或创建小镇后,场景能正常显示。
- 完成模型选择并邀请居民,开始运行;观察居民是否出现后续行动、想法或对话。模型不可用时相关决策会暂停,画面存在并不代表模型已接通。
- 返回「选择小镇」保存并关闭当前小镇,再次进入,确认居民和世界状态仍在。
需要检查当前服务来源时,打开健康接口:
curl http://127.0.0.1:5174/api/towns/health
开发文档指出,响应中的 source 字段可用于确认实际运行的工作区。不要把含本机路径的原始响应直接粘进公开 Issue。
浏览器版与 macOS 应用共用默认数据目录,避免同时打开同一份数据;新版不自动迁移旧 Godot 存档。完整路径与覆盖方式见下文「存档、数据目录和模型文件」。
开发模式与常见问题
要修改前端时,完成依赖安装及 Rust 构建后,把最后的启动命令改成:
corepack pnpm dev
开发模式启动 Vite,管理服务默认端口为 8788;浏览器地址以终端打印结果为准。preview 默认由管理服务在 5174 提供构建产物,二者不要混淆。源码更新后使用 preview,应重新执行 Rust 和前端构建,避免运行旧产物。
| 现象 | 优先检查与处理 |
|---|---|
| Rust 编译时提示方法不存在或不支持 edition | 确认使用 cargo +1.88.0,不要依赖本机旧的默认工具链 |
| preview 提示构建过期 | 重新运行 corepack pnpm build;涉及 Rust 修改时也重新编译内核 |
| 设置中没有内置模型 | 确认 target/builtin-model/llama-server 已生成,再重启管理服务 |
| 内置模型存在但居民不做决定 | 确认权重下载完成、居民已绑定该模型、小镇处于运行状态;检查对应小镇的 logs/builtin-model.log |
| 端口已占用或页面像旧版本 | 检查健康接口 source,确认是否连接了另一个工作区;停止自己启动的旧服务后再重试 |
| 场景存在却没有居民 | 新世界初始可以是空村,需要从村民库邀请居民 |
上述排查项来自项目配置、脚本与开发文档;本文没有给出机器内存下限、启动耗时或推理速度的实测结论。
Windows 源码启动器
Windows 目前仍是实验性支持。已准备源码运行环境时,可在 Windows 上执行:
node scripts/windows/build-local-launcher.mjs
生成的 EXE 仍依赖 Node.js、已编译的 Rust 程序、前端依赖以及本地 Ollama 与现有模型;不会自动下载模型,也不能脱离项目目录使用。它打开浏览器,与需要 WebView2 的完整 Windows 应用包是两条路径。平台验证边界见下文「平台状态与许可」。
技术栈演变理解
Godot 线的最后快照是 e8a1ee3c(2026-10-08)。该树的顶层是 game/ 与 tools/,游戏、界面和模型请求都在 Godot 工程里。2026-10-07 的本文初稿固定在更早的 43ef1d13,它是 e8a1ee3c 的祖先,同样属于 Godot 历史,不是现在的 main。
e8a1ee3c 也在当前 main 的历史上。之后的提交拆出 crates/ 与 frontend/,并用 HD-2D 替换 Godot 画面。到点唤醒、模型给出结构化决定、世界提交后才成为事实,这三步还在。决策和画面不再放在同一个 Godot 进程里。
Godot 单体客户端(至 e8a1ee3c) | 当前 main(f2b8f5f1) | |
|---|---|---|
| 进程 | 一个 Godot 客户端包办场景、规则和 HTTP | 管理服务按小镇启动 host;页面只连本机 |
| 世界状态 | GDScript 的 World 运行时,由场景循环调用 advance | World 只编排时间和命令,不产生决策 |
| 居民决策 | Agent 协议、wake_packet、HTTPRequest 节点 | ResidentRuntime::poll,工作线程里用阻塞 HTTP |
| 画面 | CharacterBody2D、精灵和 Godot UI | Three.js 立体场景加像素立牌;点击和名字在 SVG 叠层 |
| 几何 | 表现路径和权威分钟位置分开存 | 协议禁止居民意图携带绝对坐标;行走几何由 host 编译后注入内核 |
| 模型失败 | 43ef1d13 上有限重试后,可用脚本活动续上;e8a1ee3c 的这一处本文没有再逐行核对 | 没有可用模型就暂停该居民的决策,不用脚本替他过日子 |
| 分发 | Godot 导出 Windows、macOS、Android | 同一套前端;壳只负责拉起管理服务和 WebView 或浏览器 |
| 存档 | 会话协调器写 Godot 用户目录 | 每镇 world.json;不读取、不转换旧 Godot 存档 |
| 许可 | 当时根目录还没有统一 LICENSE | 原创代码与文档为 PolyForm Noncommercial 1.0.0 |
这样拆,是因为小镇模拟里有两套时钟。世界按模拟分钟走,一步里要结算活动、需求和天气;模型一次请求可以等到数百秒,而且多个居民会同时在途。把推理放进画面帧,画面会被一次补全拖住;把世界事实放进模型文本,居民说「送到了」就会污染记忆。Rust 内核留在世界进程的主循环里做 step,模型调用放进有界线程池。页面只消费名册和命令回执,所以浏览器、Swift 窗口、Android WebView 和 Windows WebView2 可以共用同一套 TypeScript。
Godot 把场景、2D 身体、动画和编辑器放在同一个工程里,适合先把人在地图上走动做出来。对当时那个像素小镇,继续用 Godot 说得通。重写换来的是世界与画面分开、多个平台共用薄壳,以及 llama-server 随世界进程起停。模型等待还在。按这份 README,Windows 和 Android 也还没有实机验收。
分层、crate 与前端
工作区锁定 Rust 1.88.0,Node.js 最低 22.18(.nvmrc 为 24.14.1),包管理器是 pnpm 10.34.5。五个 crate 的依赖方向是:protocol 不依赖别人;kernel 只依赖 protocol;agent 依赖 kernel 与 protocol;host 依赖这三者并负责网络;manager 依赖 host 与 agent,用来拉起世界进程。前端没有 Rust 绑定,靠 JSON 协议。
| 层 | 目录 | 做什么 |
|---|---|---|
| 协议 | crates/protocol | ClientMsg、ServerMsg、行动草案、名册、化身与模型配置 |
| 世界 | crates/kernel | 时间、人物、导航、需求、生理、天气、物品、背包、交谈 |
| 居民代理 | crates/agent | 会话、工具、记忆、内置模型题面、额度 |
| 世界进程 | crates/host | 二进制 host 与 town-map;推进、存档、HTTP、WebSocket |
| 管理服务 | crates/manager | 二进制 town-manager;选镇 HTTP、按需启动 host |
| 共享页面 | frontend/town-shared | 协议类型、化身、设置、壳层,不依赖 Three.js |
| 立体画面 | frontend/town-2d | Vite 应用,依赖 three ^0.185.1 与 town-shared |
| 应用壳 | android/、macos/、windows/ | 启动管理服务,在 WebView 或系统窗口里打开同一页面 |
| 脚本 | scripts/ | 启动、内置模型编译、各地打包、与管理服务同构的桌面脚本 |
host 的模型 HTTP 使用 ureq,注释写明同步阻塞、由线程池调用、不引入 tokio。内置模型走本机 /completion,在线模型走 {base_url}/chat/completions。密钥只出现在内存和请求头,界面回读是掩码。
flowchart TB
A["town-manager<br/>选镇与本机页面"] --> B["host<br/>一个小镇一个进程"]
B --> C["kernel::World::step<br/>模拟分钟"]
B --> D["ResidentRuntime::poll<br/>到点才请求"]
D --> E["工作线程<br/>builtin 或在线补全"]
E --> F["on_reply<br/>核对编号与动作版本"]
F --> G["工具 run_actions / interact"]
G --> C
C --> H["ServerMsg::Roster"]
H --> I["town-2d<br/>Three.js 与点击区"]
I --> B
图里只画成功走到工具的那一条。版本对不上的回复在 on_reply 里丢掉,不会进入 run_actions。模型缺失时根本不会把请求送进线程池。
宿主怎么起来,页面分成哪两包
玩家路径不是直接 cargo run 一个画面。scripts/start-client.mjs 先拉起管理服务。preview 默认端口 5174,并先检查前端构建;dev 默认管理端口 8788,再另起 Vite。管理服务见 crates/manager/src/main.rs。进入某个小镇时,Hosts 启动 host,传入该镇的 map.json 与 world.json,并用 stdin 管道拴住它。管理服务退出时,host 读到 EOF 后先保存再退出;应用壳同样握着管理服务的 stdin,壳退出后由管理服务通知各世界保存。
host 自己的循环在 Runtime::tick:处理化身输入,按速度调用 World::step,收回已完成的模型结果,再 poll 新请求,然后广播名册。默认并发来自 Limits 的 max_concurrent: 4。有人正在等模型、有人倒下、或玩家正控制化身时不快进;大家都在做还要持续一阵的事时,速度才会往上加,接近做完时回到正常步速。
town-shared 持有命令和协议,town-2d 只负责这张地图怎么画。点选不在 Three.js 网格上做射线:ResidentLayer 用一块和像素小人一样大的透明 SVG 矩形当点击区。固定提交把这块矩形扩到身形框,所以点到身上任意位置都算点到这个人。重叠时,脚在画面上更靠下的人后绘制、叠在上面,点到的是这一位。
一条核心路径:居民决定去做一件事
问题是:模型写出的「去某处做事」,怎样才变成世界里正在进行的活动。入口在世界进程的每一拍,不在页面点击。
Runtime::tick先告诉运行器谁还没有能用的模型。缺模型的居民被标成等待,不发请求,也不记成失败。- 世界仍在跑时,
ResidentRuntime::poll_at轮转会话,先把日志摄入记忆,再看此人是否到点、是否醒着、在途数量是否低于上限。 ResidentDriver::send按该居民的模型绑定组目标。内置模型进builtin::complete;在线模型由post_completion发到/chat/completions,超时 300 秒。工作线程不读写World。- 回复回到
on_reply。请求编号、会话 epoch 或动作版本对不上,整段回复作废。 - 采纳后
execute_calls逐个执行工具。身体动作走run_actions,它只把ActionDraft交给内核。 World::run_actions整组校验后入队。意图字段是动词、处所、落脚点、相对时长或「天亮 / 天黑」,没有绝对坐标。随后World::step按活动边界推进,完成与失败都写成世界事件。- 下一拍
MemoryStore::ingest只吃日志里已经发生的事件。检索用的 SQLite 在MemoryIndex::new里是内存库,注释写明它可以重建,恢复依据是存档中的世界事实和MemoryStore。
工具名是封闭枚举,生活轮次可以用 run_actions、interact、observe、recall 等;整理轮次只有 reflect、read_content、context_read。模型若声称做完了但没有对应的世界回执,记忆里不会多出一条「已完成」。
化身走另一条命令,不占用居民的 run_actions。页面 createAvatarCommands 发送带 request_id 的 avatar 消息;Runtime::network_command 交给化身事务;内核侧是 avatar_interact。方向输入 AvatarInput 不回执。协议注释写明:除化身姿态外,线上意图不得携带几何。
存档、数据目录和模型文件
管理服务的 --root 才是玩家数据根。其下有 towns/<小镇编号>/(map.json、world.json、host.log、logs/)、config/(含 credentials.json)、共用的 resident-library/,以及下载用的 builtin-model/。密钥写入走 atomic_write,Unix 上临时文件模式是 0600。这是文件权限,不是操作系统钥匙串,源码里也没有再套一层口令派生加密。providers.json 若混入疑似明文密钥会被拒绝保存。
| 启动方式 | 默认数据根 | 依据 |
|---|---|---|
| 源码里的管理服务,以及 macOS 应用 | ~/Library/Application Support/MyAiTown;其他桌面未设置时走 XDG_DATA_HOME 或 ~/.local/share/my-ai-town | scripts/towns/storage.mjs、TownRuntime.swift |
| Windows 应用壳 | %LOCALAPPDATA%\MyAiTown | windows/src/runtime.rs |
| Windows 源码启动器 | 项目里的 .local/run-data,可用本机配置改 | README「Windows 本地启动器」 |
| Android | 应用私有目录 files/home,作为 --root 传入 | TownRuntime.java |
AI_TOWN_HOME 覆盖上述根目录。单独运行 host 且不传 --save-file 时,application::data_root 还会在 macOS 路径后加 town-2d。正常选镇不走这条,存档路径由管理服务传入。
内置模型的服务编号是 builtin,格式号 FORMAT = 1。推理程序不在仓库里,由 node scripts/builtin-model/build.mjs 编译固定版本的 llama.cpp:电脑用 Metal,Android 用 CPU,Windows 用 CPU 且要求 AVX2。权重约 529 MB,发布处是 BUILTIN_SOURCE(魔搭 hihuazi/ai-town-villager 的 master 清单),第一次使用时下到用户目录的 builtin-model/。题面在 crates/agent/src/builtin.rs 按训练模板渲染,不交给 llama.cpp 的聊天模板。一句话建镇和写人设仍调用在线模型。Windows 源码启动器另可拉起本机 Ollama(默认模型名 qwen3.5:2b),那是启动器路径,不是上面的 builtin 权重。
平台状态与许可
下表是 README「平台状态」的转述,不是本文的实机测试。
| 平台 | 仓库自己的说法 |
|---|---|
| macOS 浏览器 | 维护者在 Apple 芯片的 Mac 上完成过源码编译、Chrome 启动和核心流程检查 |
| macOS 应用 | Swift 壳与 DMG 脚本,目标 macOS 13+、Apple 芯片;这次源码发布不附带已签名或公证的 DMG |
| Android | 壳与 APK 脚本,目标 Android 8.0+、arm64;内置模型还要求 armv8.2-a、dotprod、fp16。本次发布未重新做手机实机验收 |
| Windows | 尚未实机测试。目标 Windows 10/11 x64 与 WebView2。Mac 上交叉编译成功不能代替实机 |
| Linux 及其他 | 未在这次发布中验证。CI 通过不等于这些平台能运行 |
CI 工作流是 test.yml、format.yml 和 protect-crates.yml。test.yml 在 Ubuntu 上跑 Rust fmt、Clippy、测试,以及前端的 typecheck 与测试。protect-crates.yml 拒绝外部 PR 修改 crates/。当前树里没有 Godot 导出,也没有 Steam 上传步骤。
原创代码与文档使用 PolyForm Noncommercial 1.0.0:源码公开,许可定义的非商业用途可以学习、修改和分享,商业使用要另取授权。第三方依赖、Kenney 音效、llama.cpp 和下载的模型沿用各自许可,见 NOTICE 与 licenses/。
关联阅读与源码复核
当前客户端已经是浏览器里的 Three.js 页面,不再是 Godot 场景。若要比较「只画 2D」和「自带相机、场景生命周期的 Web 游戏框架」,读 Phaser 与 PixiJS:Web 2D 框架的能力、开发状态与选型。本文的画面层更接近自定义绘制加 DOM 界面,世界规则不在这两个框架里。
若要比较独立游戏的引擎、素材和发行路径,读 用 Agent 与 AI 开发 Steam 独立游戏:技术选型、素材流程与开发者申请。my_ai_town 现在提供的是源码和打包脚本;Godot 时期的预发布包不能当成 HD-2D 的 Steam 构建。
要自己核对本文,克隆仓库后执行 git checkout --detach f2b8f5f1aa14bbcf10f9924b1c0c9c23ccaae5d1,再按 docs/README.md 的检查一节 运行。那些命令需要 Rust 1.88.0 与 Node.js 22.18 以上。本文没有跑过它们,因此不能把 CI 文件的存在写成「这个提交的测试已经在这里复现通过」。