用 Rust 编写 Craft Anything WASM 检查插件:readme_says_how_to_run 实战 【免费下载链接】craft-anythingCraft Anything: understand how any software works, and craft your own.项目地址https://gitcode.com/gh_mirrors/cr/craft-anything点击查看免费下载Craft Anything 的 WASM 插件体系允许你用一个.wasmcomponent 为构建流程增加工具或“检查项check”而无需改动宿主代码。本文以仓库中的示例插件 examples/wasm-plugin-readme-check 为主线完整讲解一个check 插件从 WIT 接口、Rust 实现、编译打包、配置加载到测试验证的全链路并给出宿主侧的运行语义与边界条件读完你可以独立写出并交付自己的检查插件。这个插件到底做什么示例插件只有一个检查readme_says_how_to_run。它判断“这次构建产物根目录下是否有README.md并且这份 README 是否告诉使用者如何启动它”如果不满足就返回一条人类可读的修复建议。文档原文的定位非常明确A Craft Anything check plugin written in Rust. Its one check,readme_says_how_to_run, passes when the build has aREADME.mdat its top that tells a person how to start it, and otherwise says what to add.它有三个关键设计约束直接决定了插件的能力边界实现craft-anything:plugin/checks不是tools接口定义在 crates/engines/wit/plugin.wit不 import 任何东西拿不到文件系统、网络、环境变量和时钟唯一的信息来源是宿主塞给它的 JSON只索取.md文件的文本extensions: [md]其他文件只以“路径 大小”的形式出现。这三条组合起来意味着插件既不能自己读盘也不能偷偷联网审查逻辑完全运行在一个受 fuel 与内存约束的沙箱实例里。检查契约读一遍 plugin.witplugin.wit用 semver 给插件 API 编版本并注释了向前兼容规则package craft-anything:plugin0.1.0; /// Ways of judging a build, run after the built-in checks of each round. interface checks { describe: func() - string; run: func(name: string, build: string) - string; } world checks-plugin { export checks; }contacts之外还有tools接口与三个 worldplugin、checks-plugin、full-plugin插件按自己需要导出。示例插件用的是checks-plugin所以只导出检查能力。describe 的返回格式describe()返回一段 JSON 数组每个元素描述一个检查项。字段语义在 WIT 注释里写得很清楚字段类型含义namestring检查名与插件 id 拼接后成为全局检查 idtitlestring展示给人看的标题descriptionstring说明这个检查在判断什么kindsstring[]适用的项目类型website、app、game…空或缺省表示“全部适用”extensionsstring[]希望读到正文的文件扩展名不带点其余文件只给路径与大小示例的实现就是一段朴素而完整的 JSON 字面量src/lib.rsfn describe() - String { json!([{ name: readme_says_how_to_run, title: README says how to run it, description: The build has a README.md that tells a person how to start it., kinds: [], extensions: [md] }]) .to_string() }注意kinds: []它让这个检查对所有类型的项目生效。如果只关心游戏或网站可以写成[game]、[website]——宿主会按kind过滤不匹配的根本不会被调用见下文“宿主如何调度”。run 收到的 build JSONrun(name, build)的build是宿主序列化好的构建快照WIT 注释给出了完整形状{ request: …, kind: game, stack: Godot, round: 1, files: [{path: src/main.rs, size: 1200, text: …}], truncated: false }各字段含义request用户最初的原始需求文本kind本轮判定的项目类型由宿主的 kind 决定与PluginJudge传下来的kind一致stack技术栈标识如Godot、Rustround当前是第几轮构建u32从 1 开始files构建目录下的文件列表path是相对构建目录、用/分隔的路径只有插件在extensions里点名的扩展名才带texttruncated是否发生了任何截断文件数、单文件文本、文本总量。run 的返回格式成功时返回{passed: true, similarity: 87.5, summary: …}passed布尔必须有宿主会校验缺失则视为插件输出格式错误similarity可选0–100越小越不相似宿主会做is_finite与clamp(0, 100)处理summary给人看的说明。失败时返回{error: …}宿主会把它包装成工具错误。从零实现judge 函数的四道判断真正的判定逻辑集中在judgesrc/lib.rs。它按顺序做了四件事。第一步解析 JSON 并取文件列表。let b: Value serde_json::from_str(build).map_err(|e| format!(the build isnt JSON: {e}))?; let files b.get(files).and_then(Value::as_array).ok_or(the build has no files list)?;第二步找根目录的 README。let readme files.iter().find(|f| { f.get(path).and_then(Value::as_str) .is_some_and(|p| p.eq_ignore_ascii_case(README.md)) });这里用eq_ignore_ascii_case与完整相对路径比较所以只有根目录的README.md大小写任意如readme.md能命中docs/README.md这类嵌套文件因为路径不等于README.md不会被误判成根 README。这也解释了文档里 aREADME.mdat its top 的措辞。找不到时直接返回失败与建议{passed: false, summary: Theres no README.md at the top of the build: add one that says what it is and how to start it.}第三步检查正文是否拿到了。let Some(text) readme.get(text).and_then(Value::as_str) else { return Ok(json!({passed: false, summary: README.md is too large to read; keep it short.})); };只有text缺失才走到这里而text缺失的唯一原因是该文件超过了宿主的单文件文本上限见下文 256 KiB。所以这条提示语精确对应一个可修复的问题README 太长了。第四步三个内容判定按优先级短路。const RUN_WORDS: [str] [run, start, open, play, launch, npm , cargo , python, ./]; let lower text.to_lowercase(); let says_how RUN_WORDS.iter().any(|w| lower.contains(w)) || text.contains(); Ok(if text.trim().len() 40 { json!({passed: false, summary: README.md is nearly empty: say what this is and how to start it.}) } else if says_how { json!({passed: true, summary: README.md says how to start it.}) } else { json!({passed: false, summary: README.md doesnt say how to start it: add the command or the steps.}) })判定规则可以整理为条件结果提示语指向的修复动作text.trim()长度 40失败README 几乎空白需要说明这是什么、怎么启动命中RUN_WORDS中任意词先转小写通过—或包含 Markdown 代码围栏通过—其他失败补上启动命令或步骤为什么“包含代码围栏”也算通过因为 README 里贴一段 shell 命令块本身就是在告诉人怎么跑这比维护一份动词表更通用。RUN_WORDS里刻意保留了尾随空格的npm 、cargo 、python 是为了降低误报——比如run会命中runtime但cargo不会命中cargox。未知名与异常的分层处理run的门面函数把“插件不认识这个名字”与“判定过程出错”分开处理let answer match name.as_str() { readme_says_how_to_run judge(build), other Err(format!(this plugin has no check called {other})), }; match answer { Ok(v) v, Err(e) json!({error: e}), }这符合 WIT 契约返回值永远是字符串错误通过{error: …}表达而不是让 wasm 实例 trap。宿主侧run_check会把 JSON 解析失败、error字段、缺少passed分别映射成不同的错误码便于定位问题crates/engines/src/wasm_plugin.rs。构建三步产出 componentREADME 给出的构建命令是rustup target add wasm32-unknown-unknown cargo build --release --target wasm32-unknown-unknown wasm-tools component new target/wasm32-unknown-unknown/release/wasm_plugin_readme_check.wasm -o readme-check.wasm三条命令分别对应三件事安装wasm32-unknown-unknown目标Rust 的标准 wasm32 目标以 release 模式编译出核心模块core module产物名由 Cargo 包名推导所以是wasm_plugin_readme_check.wasm用wasm-tools component new把核心模块包装成component输出readme-check.wasm。第 3 步不可省略。宿主只接受 component宿主读取的是component_type()的导出直接把核心模块配进去会在加载阶段就被拒绝错误码是invalid并附带 “Build the plugin as a component exporting …” 之类的修复建议。Cargo.toml 里三个容易忽略的细节examples/wasm-plugin-readme-check/Cargo.toml 很短但每一条都是必要的[workspace] # 空的 workspace 表声明这是一个独立 crate不并入上层 Cargo workspace [lib] crate-type [cdylib] # 产出 wasm 动态库而不是 Rust 的 rlib [dependencies] wit-bindgen 0.63 serde_json 1 [profile.release] opt-level s lto true strip true[workspace]注释写得很直接——“Not part of the Craft Anything workspace: it builds for wasm32-unknown-unknown on its own.”。不写这一行cargo build会尝试用仓库根 workspace 的配置而根 workspace 并未为 wasm32 目标准备。crate-type [cdylib]wit-bindgen生成的export!宏需要动态库导出符号rlib不适用。release profile 三件套opt-level s优先体积、lto跨 crate 优化、strip去掉调试符号。由于插件文件本身有 64 MiB 的加载上限体积优化更多是运行效率与分发便利的考虑。wit-bindgen 如何找到 WITwit_bindgen::generate!({ path: ../../crates/engines/wit/plugin.wit, world: checks-plugin, }); struct Plugin; impl exports::craft_anything::plugin::checks::Guest for Plugin { … } export!(Plugin);path是相对插件 crate 目录的路径world选择checks-plugin——因为示例只导出checks。生成代码后Rust 侧的类型是exports::craft_anything::plugin::checks::Guest与 WIT 的craft-anything:plugin/checks一一对应。若你想同时提供工具和检查把world换成full-plugin并同时实现tools::Guest与checks::Guest。配置加载让宿主认识你的插件README 的第二部分是使用方式只有两句但信息密度很高Craft Anything built with--features wasm-plugins, and inconfig.toml(or Settings → Plugins):[[plugins.wasm]] id readme path /full/path/to/readme-check.wasm前提必须启用 wasm-plugins 特性WASM 支持是可选特性因为它会编译 wasmtime构建代价大。特性沿着依赖链逐级传递crates/engines/Cargo.tomlwasm-plugins [dep:wasmtime]且wasmtime为optional truecrates/engine/Cargo.tomlwasm-plugins [craft-anything-engines/wasm-plugins]apps/craft-anything/Cargo.toml 与 apps/craft-anything-cli/Cargo.toml 再各自透传。未启用该特性的构建不会失败而是跳过并记录错误日志// crates/engine/src/wasm_plugins.rs pub fn install(_session: mut Session, configs: VecWasmPluginConfig, _tools: mut VecRuntimeTool) { for p in configs { log::error!(WASM plugin {} skipped: this build has no WASM support (build with --features wasm-plugins), p.id); } }所以“配置了却没生效”的第一排查项永远是这个二进制是不是带--features wasm-plugins编出来的。配置字段全貌宿主侧的配置结构体 WasmPluginConfig 有四个字段字段必填默认值说明id是—工具集 id也是工具/检查 id 的前缀只允许小写字母、数字与_name否等于id人类可读名称空白字符串会被忽略回退到idpath是—component 文件的绝对路径扩展名必须是.wasm大小写不敏感enabled否true设为false可保留配置但停用problem()把配置错误的检查固化成三条明确提示这也是你在日志里会看到的内容if !RuntimeTool::valid_id(self.id) || crate::registry::custom_toolset(self.id).is_none() { return Some(format!(Plugin id {:?} must be lower-case letters, digits and _., self.id)); } if !self.path.is_absolute() || !self.path.extension().is_some_and(|e| e.eq_ignore_ascii_case(wasm)) { return Some(format!(The {} plugin needs the full path of its .wasm file., self.id)); } if !self.path.is_file() { return Some(format!(The {} plugins file isnt there: {}., self.id, self.path.display())); }注意第二、三条相对路径不被接受扩展名不是.wasm不被接受文件不存在也不被接受。文档里path /full/path/to/readme-check.wasm的措辞不是随口写的。检查 id 的拼装规则README 中有一句非常关键的说明Its check is thenreadme_readme_says_how_to_runin each builds check results.也就是plugin id_check name。因为配置里id readme、检查名是readme_says_how_to_run所以最终 id 是readme_readme_says_how_to_run读起来有点重复属于命名巧合而非 bug。宿主实现里对应let id format!({}_{}, p.id, c.name); if !RuntimeTool::valid_id(id) || checks.0.iter().any(|x| x.id id) { log::error!(WASM plugin {} check {:?} skipped (its id {id:?} is invalid or taken), p.id, c.name); continue; }同时还有两条硬约束单个插件最多 500 个检查/工具MAX_TOOLS以及 id 冲突会被跳过并记日志。工具 id 还额外truncate(64)检查 id 没有截断但同样要过valid_id。通过 GUI 配置除手写config.toml也可以在Settings → Plugins里管理。界面把 MCP 与 WASM 两类插件并列展示WASM 条目会显示名称、.wasm路径和problem()的诊断文本并提供开关与删除按钮crates/ui-egui/src/settings_ui.rs。界面上的说明也点明了沙箱性质WebAssembly components (a .wasm file, which cant touch your files or the network).并且明确开关状态从下次启动生效。宿主如何调度你的检查理解调度顺序才能写出“会被真正调用”的检查。顺序内置检查在前插件检查在后run_checks把两类检查串成一条迭代链crates/ai/src/rebuild/kinds/mod.rslet from_plugins craft_anything_engine::wasm_plugins::checks(self.engine.session()); let plugin_checks from_plugins .into_iter() .filter(|c| c.kinds.is_empty() || c.kinds.iter().any(|k| k kind.id())) .map(|check| Box::new(plugin_check::PluginJudge { check, kind: kind.id() }) as Boxdyn Check); for c in kind.checks(ws).into_iter().chain(plugin_checks) { if !c.applies(cx) { continue; } let r c.run(mut cx)?; let failed !r.passed; out.push(r); if failed { break; } }要点每轮先跑该 kind 的内置检查再跑插件检查与 README 的 Checks run after the built-in checks of each round 一致kinds非空时按kind.id()过滤空则全通过任一检查失败即中断本轮后续检查——因为后面的检查可能依赖前面已经成立的事实例如截图检查需要构建成功。这也意味着如果你的插件检查排在中间且返回失败后面的插件检查不会被调用。快照生成build_json 的限制插件看到的files由build_json生成crates/ai/src/rebuild/kinds/plugin_check.rs其中的常量决定了“什么内容能被插件看到”常量值作用MAX_FILES20 000最多列出的文件数超出则置truncatedMAX_TEXT256 KiB单个文件可提供的文本上限MAX_TOTAL_TEXT8 MiB所有文件文本总量上限目录深度32超过深度不再下钻SKIP_DIRSnode_modules、target、.git、dist、build、Library、Temp、obj、bin、.godot、Intermediate、Saved、DerivedDataCache、__pycache__、.venv依赖、缓存与构建产物目录整体排除另外两个细节值得写进你的插件设计里路径分隔符统一替换为/replace(\\, /)所以 Windows 上也不会拿到反斜杠文件列表按相对路径排序后再生成因此find的“第一个匹配”是可复现的。扩展名匹配是大小写不敏感的并且会去掉配置里可能残留的前导点let wanted path.extension().and_then(|e| e.to_str()) .is_some_and(|e| extensions.iter().any(|x| x.trim_start_matches(.).eq_ignore_ascii_case(e)));所以[md]与[.md]、[MD]等效。失败了会怎样检查本身的健壮性设计PluginJudge::judge有一个非常人性化的策略plugin_check.rsErr(e) CheckResult::new(self.check.id, round, true, format!(The {} check couldnt run: {}, self.check.title, e.message)),即插件崩溃/超时/输出不合法时不判构建失败而是记为“通过 说明原因”。注释解释了理由避免把 builder 派去修一个与构建无关的问题。这与“检查返回passed: false”是两回事——后者是明确的判定失败会真的让构建方去修。成功路径上还做了防御性归一化passed缺失或不是 bool → 视为falsesummary缺失 → 通过时Passed.否则Didnt pass.similarity必须有限并clamp(0.0, 100.0)测试里用 140 验证会被压成 100最终 note 会拼成{title}: {summary}。CheckResult的字段定义在 crates/engine/src/workspace.rs其中注释强调 Decisions readpassedandsimilarity, nevernote(which is for people)——这也提醒插件作者summary是给人看的不要试图用文案影响决策。沙箱为什么插件的工具可以“不问就跑”WASM 插件的安全模型是整个设计的核心卖点。README 的一句 imports nothing 背后是一整套限制crates/engines/src/wasm_plugin.rs限制取值位置导入空Linker::new(engine)任何 import 都会实例化失败from_bytes每次调用的 work 预算FUEL_PER_CALL 2_000_000_000常量内存上限MAX_MEMORY 256 MiB常量实例数上限16StoreLimitsBuildercomponent 文件大小64 MiBMAX_FILE返回 JSON 大小16 MiBMAX_OUTPUT实例复用每次调用新建 store instanceinstance()不导入任何东西意味着 WASI 也没链上文件、网络、环境变量、时钟一概没有“唯一出口是它返回的 JSON”。每次调用新建实例则保证调用之间没有状态残留——测试里连续两次echo得到相同结果正是为了验证这一点。超时的判定方式很直接let out_of_fuel store.get_fuel().is_ok_and(|f| f 0); if out_of_fuel { ToolError::new(ErrorCode::OverLimit, format!(The plugins {name} ran past its work limit and was stopped)) }死循环插件因此会被以OverLimit中止而不是拖死宿主。版本兼容0.x 的 minor 就是 breakingcompatible(found, ours)的规则match (parse(found), parse(ours)) { (Some((0, a)), Some((0, b))) a b, (Some((a, _)), Some((b, _))) a b, _ false, }即主版本相同当主版本为0时次版本也必须相同。所以0.1.0与0.1.7可以互通0.2.0不行。不兼容不会被静默跳过而是给出明确错误The plugin exports craft-anything:plugin/checks for API 0.2.0; this Craft Anything speaks 0.1.0.并附修复建议“按本版本的 plugin.wit 重新构建或使用匹配的 Craft Anything”。补丁版本只允许新增可选 JSON 字段——这要求插件侧也要忽略不认识的字段宿主侧同样如此。验证用环境变量喂入真实产物README 的最后一段给出端到端验证命令CRAFT_ANYTHING_TEST_WASM_CHECK/full/path/to/readme-check.wasm \ cargo test -p craft-anything-engines --features wasm-plugins the_example_check对应的测试是the_example_check_reads_the_readmecrates/engines/src/wasm_plugin.rs。它用环境变量做“有则跑、无则跳过”的门控因此 CI 里没有真实产物时不会失败let Some(path) std::env::var_os(CRAFT_ANYTHING_TEST_WASM_CHECK) else { return };测试覆盖了四类行为正好可以当作你自测的清单场景构造期望导出能力加载 componentexports() (false, true)只有检查没有工具描述正确describe_checks()name readme_says_how_to_runextensions [md]合格 README# Snake\n\nA small snake game.\n\nRun it withcargo run --release.passed true含糊 README# Snake\n\nA small snake game made for fun, with apples.passed false且 summary 含 how to start无 README只有src/main.rspassed false未知名run_check(nope, …)返回Err其中含糊 README 的构造尤其巧妙它长度超过 40 字符、不含任何启动动词、没有代码围栏精准落在“内容太短”之外的失败分支。同一文件里还有不依赖真实产物的单元测试覆盖了“描述工具并响应调用”“死循环插件被中止”“import 与垃圾输入被拒绝”“版本兼容矩阵”等可以在没有编译 wasm 的情况下直接跑。排错清单现象可能原因处理日志出现 “this build has no WASM support”二进制未启用特性用--features wasm-plugins重新构建日志出现 “needs the full path of its .wasm file”path是相对路径或扩展名不对改成绝对路径确保后缀.wasm日志出现 “file isnt there”路径写错或产物未生成确认wasm-tools component new的输出存在出现 “exports … for API 0.2.0; this Craft Anything speaks 0.1.0”插件与宿主 API 版本不兼容按当前plugin.wit重建或换匹配的宿主出现 “not a usable WASM component”直接用了核心模块没做 component 化补wasm-tools component new这一步插件加载成功但检查不出现kinds与当前项目类型不匹配检查kinds是否写成了[game]等具体值README 明明存在却判“没有”README 不在根目录或位于SKIP_DIRS内放到构建根目录node_modules、dist等目录不会出现在files中提示 “too large to read”README 超过 256 KiB精简文件或只保留必要章节检查名为x_y而非预期id 是plugin id_check name按拼接规则核对id readme会得到readme_readme_says_how_to_run检查通过但 note 说 “couldnt run”插件执行失败被策略性记为通过查看错误信息通常是超出 fuel、返回非法 JSON 或缺少passed相关实现路径速查示例插件examples/wasm-plugin-readme-check/src/lib.rs、examples/wasm-plugin-readme-check/Cargo.toml接口定义crates/engines/wit/plugin.wit宿主加载与调用crates/engines/src/wasm_plugin.rs配置与安装crates/engine/src/wasm_plugins.rs快照生成与调度crates/ai/src/rebuild/kinds/plugin_check.rs、crates/ai/src/rebuild/kinds/mod.rs检查结果结构crates/engine/src/workspace.rs插件系统总览与 API 版本规则docs/extending.md工具类插件对照示例examples/wasm-plugin-wordcount小结这个示例虽小却是 Craft Anything 插件体系的完整切面WIT 定义契约、wit-bindgen生成绑定、wasm-tools做 component 化、[[plugins.wasm]]完成注册、PluginJudge把构建快照喂进去并回收判定。若要把它改成你自己的检查改动集中在三处describe()里声明name/kinds/extensionsrun()的分发分支加上新名字以及judge的判定逻辑替换成你关心的规则。剩下的一切——沙箱、超时、版本协商、失败降级——宿主已经替你处理好了。赞分享【免费下载链接】craft-anythingCraft Anything: understand how any software works, and craft your own.项目地址https://gitcode.com/gh_mirrors/cr/craft-anything点击查看免费下载相关推荐MinerU 接入 RAGFlow把 MinerU 设为知识库默认 PDF 解析器实战指南MinerU 接入 RAGFlow把 MinerU 设为知识库默认 PDF 解析器实战指南 扫描件 PDF 丢进知识库切出来的 chunk 要么乱码要么半页人工智能大模型计算机视觉OCR后端三分钟上手AssemblyScript编写Higress WASM插件实战指南三分钟上手AssemblyScript编写Higress WASM插件实战指南 还在为网关定制化功能发愁还在纠结如何为云原生网关添加个性化逻辑HigresAPI网关后端云原生微服务LLM 网关人工智能MCP 服务Craft Anything 实战指南用纯 Rust 逆向引擎完成 Study → Build → Check 的软件重建Craft Anything 实战指南用纯 Rust 逆向引擎完成 Study → Build → Check 的软件重建 Craft Anything 是一上一篇SuperClaude Framework MCP 服务器安装与配置实战指南基于 Claude Code 的 MCP Server 全流程管理下一篇three-bmfont-text实战构建3D文字场景的完整案例与代码解析创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考