OpenClaw二次开发实战:从插件定制到国产大模型适配 1. 为什么必须亲手改 OpenClaw——从“开箱即用”到“专属智能体”的真实分水岭你第一次打开 OpenClaw点几下就调通了大模型、执行了命令、连上了天气 API心里一热“这工具真香”但很快现实就给你泼了盆冷水企业内网系统没开放公网接口AI 回复里总夹带英文术语UI 上那个“重置会话”按钮位置反人类更别说想让 AI 主动推送每日销售简报——这些需求官方文档里连影子都找不到。这时候你才真正意识到OpenClaw 不是“用完即走”的玩具而是一套可塑性极强的AI 智能体底座。它的价值不在于预设功能多炫酷而在于你能否把它拧成自己业务流水线里的一颗精密螺丝。我带过 7 个不同行业的二次开发项目从制造业设备巡检助手到律所合同风险初筛工具再到高校科研文献摘要生成器无一例外最终交付的都不是“OpenClaw 加了个插件”而是“一个叫‘智巡通’‘法眼初筛’‘研读快’的独立产品”。这背后的核心动作就是二次开发。它不是程序员的专利而是所有想把 AI 真正落地进业务毛细血管的人必须掌握的“最后一公里”手艺。本文讲的不是“如何看懂源码”而是“如何在不破坏系统稳定性的前提下精准地、可维护地、可升级地在关键节点上打上自己的补丁”。你会看到一个天气插件的注册背后是整个插件生命周期管理的设计哲学一条自定义 CLI 指令的添加牵扯着命令解析器与日志系统的耦合逻辑而适配通义千问远不止是换掉一个 URL而是要理解 OpenClaw 如何用统一的ModelRequest/ModelResponse接口把千差万别的大模型 API “翻译”成自己能听懂的语言。这不是教科书式的理论推演而是我在 Gitee 仓库提交了 217 次代码、回滚过 3 次主干、被测试环境凌晨三点的告警电话叫醒过 5 次之后亲手验证过的每一步。接下来的内容没有一句废话每一个命令、每一行代码、每一个配置项都对应着一个真实踩过的坑和一次成功的交付。2. 开发环境别让第一步就卡死在依赖地狱里2026 年实测最稳方案很多人卡在第一步不是因为技术不行而是败给了环境。Node.js 版本冲突、pnpm 镜像失效、Git 子模块拉取失败……这些看似琐碎的问题足以消耗掉新手一整天的热情。我见过太多人在pnpm install卡在 87% 时选择放弃。所以这一节我只讲 2026 年当下最可靠、最省心的方案所有步骤均在 macOS Sonoma 14.5、Ubuntu 24.04 LTS 和 Windows 11 23H2 上实测通过。2.1 核心依赖版本不是“≥”而是“必须等于”OpenClaw 的 monorepo 架构对依赖版本极其敏感。node -v ≥ 22.0.0这句话很多教程一笔带过但实际中v22.14.0和v22.15.0可能就因为 V8 引擎的一个小更新导致packages/ui的 Vue Devtools 插件热更新完全失效。我的建议是严格锁定为v22.14.0。这不是保守而是基于血泪教训。pnpm -v ≥ 9.0.0同理v9.12.2是目前最稳定的版本它能完美处理packages/core和packages/runtime之间复杂的 peerDependencies 关系。至于git -v ≥ 2.40.0重点在于其对 sparse-checkout 的支持这是国内镜像加速的关键。下面给出一套零失误的安装脚本# macOS (使用 Homebrew) brew install node22 brew unlink node brew link --force node22 npm install -g pnpm9.12.2 brew install git2.40 # Ubuntu/Debian (使用 NodeSource 官方源) curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - sudo apt-get install -y nodejs sudo npm install -g pnpm9.12.2 sudo apt-get install -y git # Windows (使用 Scoop) scoop install nodejs-lts22.14.0 scoop install pnpm9.12.2 scoop install git提示执行完后务必验证node -v输出为v22.14.0pnpm -v为9.12.2。任何偏差都可能导致后续构建失败不要心存侥幸。2.2 源码拉取为什么 Gitee 镜像比 GitHub 官方库更值得信赖官方 GitHub 仓库https://github.com/OpenClaw/OpenClaw.git在 2026 年上半年已基本停止维护所有新功能、Bug 修复和安全补丁都发布在 Gitee 镜像https://gitee.com/OpenClaw-CN/openclaw-cn.git上。这个镜像并非简单同步而是由国内核心贡献者团队进行的增强型维护包括预编译了packages/ui的node_modules缓存避免pnpm install时反复下载vue/dev-server将packages/gateway中所有外部 API 调用如天气、股票替换为国内可用的免费替代服务在scripts/目录下新增了sync-upstream.sh脚本可一键将 Gitee 分支与 GitHub 官方最新 commit 对齐。拉取命令必须加上--depth1参数这是提速的关键git clone --depth1 https://gitee.com/OpenClaw-CN/openclaw-cn.git cd openclaw-cn注意不要用git clone --recursive。OpenClaw 的子模块如packages/ui/node_modules在 monorepo 下是通过 pnpm 的 workspace 功能管理的手动拉取子模块只会制造混乱。2.3 依赖安装与开发启动pnpm install的隐藏开关pnpm install看似简单但它背后有三个决定成败的隐藏开关官方文档从未提及--no-frozen-lockfile首次安装时必须加此参数。它会强制 pnpm 忽略pnpm-lock.yaml中的哈希值重新计算所有依赖的精确版本。这是因为 Gitee 镜像的 lock 文件可能与你的 Node.js 版本不完全兼容。--reporter ndjson加上这个安装过程会输出结构化日志便于排查卡在哪个包。当安装卡住时你可以tail -f pnpm-debug.log实时查看。--filter如果你只关心core和ui的开发可以pnpm install --filter openclaw/core --filter openclaw/ui跳过cli和runtime节省 40% 时间。完整的、最稳妥的安装命令如下pnpm install --no-frozen-lockfile --reporter ndjson启动开发服务时pnpm dev:ui和pnpm dev:gateway必须在两个独立的终端窗口中运行。这是因为dev:ui启动的是 Vite 的前端开发服务器端口 5173而dev:gateway启动的是 Express 的后端网关默认端口 3000。它们之间通过proxy配置进行通信。如果你在一个终端里用连接dev:gateway进程会因dev:ui的热重载而被意外终止。实操心得我习惯在 VS Code 中使用Terminal: Split Terminal功能左边跑 UI右边跑 Gateway。这样当 UI 报错时我能立刻在右边看到 Gateway 是否也抛出了异常从而快速判断是前端渲染问题还是后端数据返回问题。这个简单的操作能帮你把 80% 的“白屏”问题定位时间从 30 分钟缩短到 3 分钟。3. 源码结构解剖一张图看懂 OpenClaw 的“五脏六腑”与手术刀该往哪下很多开发者面对packages/目录时第一反应是“这么多文件从哪下手”。其实OpenClaw 的架构设计得非常清晰它遵循的是经典的“关注点分离”原则每一层都只做一件事并且这件事做得非常极致。把它想象成一家现代化的工厂ui是面向客户的展厅gateway是前台接待处core是中央调度室runtime是车间里的机械臂而cli则是厂长随身携带的对讲机。理解了这个类比你就知道想改客户看到的界面就去ui想控制谁能进厂、能进哪个车间就去gateway想改变生产计划AI 意图解析就去core想给机械臂加个新夹具新命令就去runtime想让厂长用语音直接下达指令就去cli。下面这张表是我根据 2026 年最新版源码commit:a1b2c3d整理出的“手术指南”它不仅告诉你每个目录是干什么的更告诉你哪些文件是绝对不能碰的“心脏”哪些是你可以自由发挥的“四肢”。目录核心作用开发重点可安全修改绝对禁区严禁修改修改后影响范围packages/ui前端用户界面Vue3 TypeScript Piniasrc/views/页面组件、src/components/通用组件、src/assets/styles/CSS 变量src/main.tsVue 实例初始化、src/router/index.ts路由守卫逻辑、src/stores/useSessionStore.ts会话状态管理核心仅影响 UI 层不影响任何业务逻辑和数据流packages/gatewayHTTP 网关负责请求路由、鉴权、日志、跨域src/middleware/auth.ts自定义鉴权逻辑、src/routes/api/v1/新增 API 接口、src/config/server.ts端口、HTTPS 配置src/app.tsExpress 应用实例创建、src/middleware/cors.tsCORS 头设置、src/utils/proxy.ts反向代理核心影响所有进出 OpenClaw 的网络请求修改不当会导致整个服务不可用packages/coreAI 智能体大脑负责意图识别、任务规划、插件调度、模型调用src/plugins/所有插件实现、src/model/modelManager.ts模型适配器注册、src/scheduler/taskPlanner.ts任务编排算法src/index.tsCore 模块导出入口、src/types/所有类型定义文件、src/utils/llmUtils.tsLLM 通用工具函数影响所有 AI 决策和执行是整个系统最核心、最敏感的部分packages/runtime执行引擎负责调用系统命令、脚本、HTTP 请求等src/commands/内置命令实现、src/executors/执行器抽象、src/permissions/权限检查策略src/index.tsRuntime 入口、src/executor/ExecutorFactory.ts执行器工厂模式核心影响所有“行动”类操作比如openclaw run script.sh或openclaw exec curl ...packages/cli命令行工具提供openclaw全局命令src/commands/自定义指令、src/utils/logger.ts日志格式化、src/config/configManager.ts配置文件读写src/index.tsCLI 入口、src/program.tsCommander 初始化仅影响命令行交互体验不影响 Web UI 和后台服务这张表的价值远不止于“知道改哪”。它揭示了一个重要事实OpenClaw 的可扩展性是通过“约定优于配置”来实现的。比如你想加一个新插件不需要去改core的任何一行核心代码只需要在packages/core/src/plugins/下新建一个.ts文件实现Plugin接口然后在index.ts里register一下。这个过程就像给工厂的调度室core增加一份新的《标准作业指导书》SOP而不用去拆解调度室的电脑主板。这种设计保证了你在深度定制的同时依然能平滑地接收上游的版本更新。我曾帮一家银行客户将 OpenClaw 改造成内部合规审查助手他们要求所有插件必须经过内部安全审计。我们就是严格遵循这个结构在packages/core/src/plugins/下开发了 12 个插件每个插件都独立打包、独立审计、独立部署最终成功上线且后续 OpenClaw 官方发布了 3 个大版本我们的插件一个都没改全部兼容。4. 实战 1开发第一个自定义插件——不只是“Hello World”而是理解插件生命周期的起点“天气查询”插件是 OpenClaw 二次开发的“Hello World”但它的意义远不止于此。它是一个完美的教学案例因为它完整地覆盖了插件开发的四个核心阶段定义Definition、注册Registration、初始化Initialization和执行Execution。很多新手只关注handler函数却忽略了init和actions数组的精妙设计。下面我将带你逐行拆解weather.plugin.ts并告诉你为什么这样写才是“生产环境友好”的写法。4.1 插件定义parameters不是摆设而是 AI 的“输入说明书”parameters: [ { name: city, type: string, required: true, description: 城市名称如北京、上海 } ],这段代码表面上是在告诉 OpenClaw “这个插件需要一个叫 city 的字符串参数”但它的深层作用是为 OpenClaw 的core层的意图识别引擎提供训练数据。当你在 UI 里输入“查一下深圳的天气”OpenClaw 的 LLM 会将这句话解析成一个 JSON 结构其中action字段会被识别为get-weather而parameters字段则会被填充为{ city: 深圳 }。这个过程之所以能成功正是因为parameters数组里的description字段为 LLM 提供了足够的上下文语义。如果description写成“城市”LLM 就无法区分“城市”是指“北京市”还是“城市级别”。所以description的写作是一门“给 AI 写说明书”的艺术。我建议的黄金法则名词 示例 限制。例如对于一个需要邮箱的插件description应该是“用户的电子邮箱地址如userexample.com必须包含 符号”。4.2 插件执行handler函数里的“防御式编程”handler: async (params) { const { city } params; try { const res await axios.get( https://api.vvhan.com/api/weather?city${encodeURIComponent(city)} ); return { success: true, data: res.data, message: 查询到${city}的天气${res.data.info} }; } catch (e) { return { success: false, message: 查询失败${(e as Error).message} }; } }这段代码新手常犯的错误是直接throw e。这在开发环境没问题但在生产环境一个未捕获的 Promise Rejection 会直接导致整个gateway服务崩溃。正确的做法是像上面这样永远返回一个结构化的、符合ActionResponse接口的对象。success: true/false是给core层的调度器看的它决定了后续是继续执行下一个动作还是进入错误处理流程。message字段则是给最终用户看的它必须是人类可读的、有温度的提示而不是冰冷的AxiosError: Request failed with status code 404。我在线上环境见过最惨的案例一个插件的message返回了Error: timeout of 5000ms exceeded结果客服人员拿着这个报错去跟客户解释客户以为是他们的网络有问题……所以message的文案一定要经过产品经理的审核。4.3 插件注册index.ts里的“单例陷阱”import { PluginManager } from ../manager/plugin; import WeatherPlugin from ./weather.plugin; const pluginManager new PluginManager(); pluginManager.register(WeatherPlugin); export default pluginManager;这段代码藏着一个巨大的陷阱new PluginManager()。PluginManager是一个单例管理器它的职责是全局唯一地管理所有插件的生命周期。如果你在index.ts里每次都new一个那么core层其他地方比如taskPlanner.ts通过import { pluginManager } from ../plugins获取到的就是一个全新的、空的实例你的WeatherPlugin就永远不会被发现。正确的写法是确保PluginManager的实例在整个应用生命周期内只有一个。OpenClaw 的标准做法是在packages/core/src/index.ts中创建并导出这个单例// packages/core/src/index.ts import { PluginManager } from ./manager/plugin; import { initPlugins } from ./plugins; // 创建单例 export const pluginManager new PluginManager(); // 初始化所有插件这个函数会自动导入并注册 index.ts 里的所有插件 initPlugins(pluginManager); export { pluginManager };然后在你的weather.plugin.ts里注册方式就变成了// packages/core/src/plugins/weather.plugin.ts import { pluginManager } from ../index; // 导入全局单例 import { Plugin, Action } from ../types/plugin; import axios from axios; const WeatherPlugin: Plugin { // ... 其他定义 }; // 直接注册到全局单例 pluginManager.register(WeatherPlugin); // 注意这里不再 export default因为注册动作已经完成实操心得我养成了一个习惯在每次开发完一个新插件后都会在packages/core/src/plugins/index.ts里加一行console.log(Plugin [name] registered)。然后启动pnpm dev:gateway在终端日志里搜索registered。如果没看到说明注册失败立刻回头检查import路径和register调用。这个小小的日志帮我避开了 90% 的“插件不生效”问题。5. 实战 2扩展自定义系统指令——让 OpenClaw 成为你终端里的“瑞士军刀”CLI命令行界面是 OpenClaw 与操作系统最直接的桥梁。openclaw my-command这样的指令看起来只是加了一条命令但它背后打通了从用户输入、参数解析、权限校验到最终执行的完整链路。很多开发者只满足于“能跑起来”却忽略了这条链路上的每一个安全关卡。本节我将以my-command为例带你深入packages/cli的底层看看如何写出一个既强大又安全的自定义指令。5.1 指令逻辑commander的高级用法与权限边界import { Command } from commander; import { logger } from ../../utils/logger; export const myCommand new Command(my-command) .description(我的自定义指令打印指定信息) .argument(message, 要打印的信息) .option(-r, --repeat times, 重复次数, 1) .action((message, options) { const repeat parseInt(options.repeat); for (let i 0; i repeat; i) { logger.info([自定义指令] ${message}); } logger.success(自定义指令执行完成); });这段代码展示了commander库的两个核心能力.argument()定义必填参数.option()定义可选参数。但真正的精髓在于.action()回调函数里的内容。注意我在这里没有使用console.log而是用了logger.info和logger.success。这是因为packages/cli的日志系统是统一的它会将所有日志按等级info, warn, error, success分类并写入~/.openclaw/logs/cli.log。这对于线上问题排查至关重要。想象一下当客户说“我的自定义指令不工作了”你只需要grep my-command ~/.openclaw/logs/cli.log就能看到完整的执行轨迹。更重要的是my-command的.action()函数是运行在当前用户权限下的。这意味着如果你在action里写了execSync(rm -rf /)那后果不堪设想。所以OpenClaw 的cli层有一个隐含的、但极其重要的设计所有自定义指令都必须通过runtime层的Executor来执行危险操作。my-command这个例子它只是打印信息属于安全操作所以可以直接用logger。但如果你要写一个openclaw backup-db指令它的action函数里就应该调用runtime提供的executeCommand方法// packages/cli/src/commands/backup-db.ts import { Command } from commander; import { executeCommand } from openclaw/runtime; // 正确引入 import { logger } from ../../utils/logger; export const backupDbCommand new Command(backup-db) .description(备份数据库) .argument(db-name, 数据库名称) .action(async (dbName) { try { // 通过 runtime 执行它会进行权限检查和沙箱隔离 const result await executeCommand({ command: mysqldump, args: [-u, root, -p, dbName], // 这里可以配置超时、工作目录、环境变量等 timeout: 30000, cwd: /var/backups }); logger.success(数据库 ${dbName} 备份成功); } catch (e) { logger.error(数据库备份失败${e.message}); } });5.2 指令注册program.parse()的时机与顺序import { program } from commander; import { myCommand } from ./commands/my-command; program.addCommand(myCommand); program.parse(process.argv);这段注册代码看似简单但顺序至关重要。program.parse(process.argv)是整个 CLI 的“启动开关”它会解析process.argv即你在终端输入的openclaw my-command Hello然后根据addCommand注册的指令列表找到匹配的myCommand并执行其action。因此addCommand必须在parse之前调用。这是一个典型的“注册-启动”模式。我曾经遇到一个诡异的 Bugmy-command总是报error: unknown command my-command。排查了整整一天最后发现是因为在packages/cli/src/index.ts里program.parse()被写在了addCommand的前面。这个错误非常隐蔽因为 TypeScript 编译不会报错只有运行时才会暴露。另一个容易被忽视的点是program是一个全局单例。packages/cli/src/index.ts是整个 CLI 的入口它只应该被node dist/cli/index.js执行一次。如果你在my-command.ts里也import { program } from commander并尝试addCommand就会导致指令被注册两次从而引发不可预知的副作用。所以所有addCommand的调用都必须集中在packages/cli/src/index.ts这一个文件里。这是一个硬性规范也是保证 CLI 行为可预测的基础。实操心得为了防止忘记注册我创建了一个packages/cli/src/commands/_registry.ts文件里面只有一行代码export * from ./my-command;。然后在index.ts里我用import { myCommand } from ./commands/_registry;。这样当我开发完一个新指令后只需要在_registry.ts里加一行export * from ./new-command;就能确保它一定会被index.ts导入并注册。这个小小的“注册中心”让我再也没错过任何一个自定义指令。6. 实战 3适配国产大模型——不是“换个 API Key”而是重构你的 AI 通信协议适配通义千问是本教程里技术含量最高的一环。它绝不是“把 OpenAI 的 URL 换成阿里云的 URL”那么简单。它是一次对 OpenClawAI 通信协议的深度解构与重构。OpenClaw 的core层本质上是一个“AI 协议转换器”。它向上为ui和cli提供统一的、简洁的ask()接口向下为各种大模型OpenAI, Anthropic, Tongyi提供标准化的request()方法。ModelAdapter接口就是这个转换器的“插槽”。本节我将带你亲手打造这个插槽并告诉你为什么TongyiAdapter的request方法里要对res.data.choices[0].message.content进行如此精确的提取。6.1 模型适配器ModelRequest与ModelResponse的契约精神// packages/core/src/types/model.ts export interface ModelRequest { messages: Array{ role: string; content: string }; temperature?: number; max_tokens?: number; } export interface ModelResponse { content: string; finishReason: string; raw: any; }这两个接口是 OpenClaw 的“宪法”。ModelRequest规定了core层向任何大模型发起请求时必须提供的数据格式ModelResponse则规定了任何大模型返回的数据必须被“翻译”成的格式。TongyiAdapter的核心任务就是在这两者之间架起一座桥。我们来看request方法的关键部分async request(req: ModelRequest): PromiseModelResponse { try { const res await axios.post( https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions, { model: qwen-turbo, messages: req.messages, temperature: req.temperature || 0.7 }, { headers: { Authorization: Bearer ${this.apiKey}, Content-Type: application/json } } ); // 关键将通义千问的原始响应映射到 ModelResponse 接口 return { content: res.data.choices[0].message.content, finishReason: res.data.choices[0].finish_reason, raw: res.data }; } catch (e) { throw new Error(通义千问请求失败${(e as Error).message}); } }这段代码的精妙之处在于return语句。res.data.choices[0].message.content是通义千问 API 的标准返回路径但ModelResponse.content是 OpenClaw 的标准。这个映射就是“协议转换”的全部意义。如果通义千问的 API 发生变更比如把choices改成data你只需要修改TongyiAdapter里的这一行core层的其他所有代码taskPlanner.ts,pluginManager.ts都无需改动。这就是“面向接口编程”的威力。6.2 适配器注册createModelAdapter的“工厂模式”实践// packages/core/src/model/modelManager.ts export function createModelAdapter(type: string, config: any) { switch (type) { case openai: return new OpenAIAdapter(config); case tongyi: // 新增 return new TongyiAdapter(config); default: throw new Error(不支持的模型类型${type}); } }这个switch语句是一个典型的“工厂模式”Factory Pattern实现。它把“创建什么类型的适配器”这个决策从具体的业务代码中抽离出来集中到了一个统一的工厂函数里。这样做有两个巨大好处可扩展性未来要加百川、智谱、月之暗面你只需要在switch里加一个case写一个新Adapter类就完成了全部工作。可测试性你可以为createModelAdapter写单元测试模拟传入tongyi断言它返回的是TongyiAdapter的实例而无需启动真实的网络请求。提示config参数的类型是any这在 TypeScript 里是不推荐的。在生产项目中你应该为每种模型定义一个具体的配置接口比如TongyiConfig并在createModelAdapter的switch分支里进行类型断言以获得更好的类型安全。6.3 配置与验证config.yaml里的“信任链”model: type: tongyi config: apiKey: 你的通义千问API Key这个 YAML 配置是 OpenClaw 启动时加载模型适配器的“钥匙”。type: tongyi告诉modelManager去工厂里找TongyiAdapterconfig.apiKey则是打开这把锁的密码。但这里有一个至关重要的安全细节apiKey绝对不能硬编码在源码里。~/.openclaw/config.yaml是用户家目录下的文件它默认是600权限只有文件所有者可读写这是保护密钥的第一道防线。在企业环境中我们通常会把这个配置文件放在一个受控的、加密的配置中心如 HashiCorp Vault然后在pnpm dev:gateway启动时通过环境变量OPENCLAW_CONFIG_PATH指向它。这样密钥就永远不会出现在 Git 仓库或开发者的本地磁盘上。验证适配是否成功最直接的方法不是看 UI 是否能对话而是在gateway的日志里搜索TongyiAdapter。当你启动服务后gateway会在初始化时打印Using model adapter: TongyiAdapter。如果没看到说明createModelAdapter没有被正确调用或者config.yaml的type写错了。这个日志是你调试模型适配问题的“生命线”。7. 打包发布从本地开发到可交付产品的最后一步开发完成只是万里长征的前半程。如何把你的定制版 OpenClaw变成一个可以分发给同事、客户甚至上架到公司内网的“产品”这才是二次开发的终极目标。pnpm build看似一个简单的命令但它背后是一整套现代前端/后端工程化流水线。本节我将为你揭开build脚本的神秘面纱并分享几个在企业交付中屡试不爽的“打包技巧”。7.1 全量构建pnpm build做了什么执行pnpm build它会依次触发以下步骤build:ui: 运行vite build将packages/ui编译成静态资源HTML/CSS/JS输出到packages/ui/dist。build:gateway: 运行tsc将packages/gateway的 TypeScript 代码编译成 JavaScript输出到packages/gateway/dist。build:core: 同样运行tsc编译packages/core输出到packages/core/dist。build:runtime: 编译packages/runtime。build:cli: 编译packages/cli并生成可执行的dist/cli/index.js。这个过程是高度并行化的。pnpm会利用workspace的依赖关系图自动确定编译顺序。例如gateway依赖core所以build:core一定会在build:gateway之前完成。你不需要手动干预。7.2 打包 CLI生成真正的“可执行文件”pnpm build:cli是一个关键步骤。它不仅仅编译代码还会执行pkg工具一个 Node.js 应用打包器将dist/cli/index.js及其所有依赖打包成一个无需安装 Node.js 即可运行的二进制文件。这对于交付给 IT 部门或非技术人员至关重要。# 打包为 macOS 可执行文件 pnpm build:cli --target macos-x64 # 打包为 Windows 可执行文件 pnpm build:cli --target win-x64 # 打包为 Linux 可执行文件 pnpm build:cli --target linux-x64--target参数指定了目标平台。pkg会自动下载对应平台的 Node.js 运行时并将其与你的代码一起打包。最终生成的文件比如openclaw-macos就是一个独立的、双击即可运行的程序。它的大小通常在 80MB 左右包含了整个 Node.js 运行时。虽然体积不小但换来的是极致的易用