/** * bundle-openclaw.mjs — 从 openclaw 源码中 bundle 出核心插件模块 * * 用法:node scripts/bundle-openclaw.mjs * * 产出:openclaw-dist/core-bundle.mjs(自包含,无外部依赖) * * 原理: * openclaw 的 dist/ 是 TS→JS 的 1:1 编译产物,模块间保留了原始 import 关系, * 依赖 openclaw/node_modules 里的大量第三方包。 * 本脚本用 esbuild 把 bootstrap.js 需要的函数从 dist/ 中提取并打成 * 一个自包含的 ESM bundle,所有内部 import 和第三方依赖全部内联。 * * 含 native addon 的包(koffi, @napi-rs/canvas 等)标记为 external, * 它们不是核心加载流程所需的,运行时缺失只会导致特定功能降级。 */ import esbuild from 'esbuild'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ROOT = path.resolve(__dirname, '..'); const OPENCLAW_ROOT = path.resolve(ROOT, 'openclaw'); const OUTPUT_DIR = path.resolve(ROOT, 'openclaw-dist'); const OUTPUT_FILE = path.join(OUTPUT_DIR, 'core-bundle.mjs'); // bootstrap.js 需要的所有导出 const BUNDLE_ENTRY = ` export { normalizePluginsConfig, applyTestPluginDefaults, resolveEnableState, resolveMemorySlotDecision, } from './dist/plugins/config-state.js'; export { discoverOpenClawPlugins } from './dist/plugins/discovery.js'; export { initializeGlobalHookRunner } from './dist/plugins/hook-runner-global.js'; export { loadPluginManifestRegistry } from './dist/plugins/manifest-registry.js'; export { createPluginRegistry } from './dist/plugins/registry.js'; export { setActivePluginRegistry } from './dist/plugins/runtime.js'; export { createPluginRuntime } from './dist/plugins/runtime/index.js'; export { validateJsonSchemaValue } from './dist/plugins/schema-validator.js'; export { clearPluginCommands } from './dist/plugins/commands.js'; export { resolveUserPath } from './dist/utils.js'; `; // 含 native addon 或平台专属的包 → 替换为空模块 stub // 不能用 external(ESM import 在 link 阶段找不到包会直接崩) // 改用 esbuild plugin 把这些 import 指向空模块 const NATIVE_STUB_PACKAGES = [ 'koffi', '@napi-rs/canvas', '@reflink/reflink', '@lydell/node-pty', '@node-llama-cpp', 'node-llama-cpp', 'playwright-core', '@homebridge/ciao', 'libsignal-client', 'better-sqlite3', ]; async function main() { console.log('=== Bundle OpenClaw 核心模块 ===\n'); if (!fs.existsSync(path.join(OPENCLAW_ROOT, 'dist'))) { console.error(`❌ openclaw/dist/ 不存在,请先在 openclaw 目录执行构建`); process.exit(1); } fs.mkdirSync(OUTPUT_DIR, { recursive: true }); console.log(`源目录: ${OPENCLAW_ROOT}`); console.log(`输出文件: ${OUTPUT_FILE}\n`); const t0 = Date.now(); // esbuild plugin:把 native addon 包和 .node 文件替换为空模块 const stubNativePlugin = { name: 'stub-native', setup(build) { // .node 文件 → 空模块 build.onResolve({ filter: /\.node$/ }, () => ({ path: 'stub', namespace: 'native-stub', })); // native 包 → 空模块 const packageFilter = new RegExp( `^(${NATIVE_STUB_PACKAGES.map(p => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')})(/.*)?$` ); build.onResolve({ filter: packageFilter }, (args) => { return { path: args.path, namespace: 'native-stub' }; }); // 空模块的内容 — 用 CJS 格式 + Proxy 兜底所有 named import build.onLoad({ filter: /.*/, namespace: 'native-stub' }, () => ({ contents: ` const handler = { get: (_, prop) => prop === '__esModule' ? true : (() => null) }; const stub = new Proxy({}, handler); module.exports = stub; module.exports.default = stub; `, loader: 'js', })); }, }; const result = await esbuild.build({ stdin: { contents: BUNDLE_ENTRY, resolveDir: OPENCLAW_ROOT, loader: 'js', }, bundle: true, format: 'esm', platform: 'node', target: 'node18', outfile: OUTPUT_FILE, // 注入 createRequire,让 CJS 包(如 debug)在 ESM bundle 中也能 require Node 内置模块 banner: { js: `import { createRequire as __bundleCreateRequire } from 'node:module'; import { fileURLToPath as __bundleFileURLToPath } from 'node:url'; import { dirname as __bundleDirname } from 'node:path'; const require = __bundleCreateRequire(import.meta.url); const __filename = __bundleFileURLToPath(import.meta.url); const __dirname = __bundleDirname(__filename);`, }, external: [ // 所有 Node 内置模块(动态获取,避免遗漏) 'node:*', ...(await import('node:module')).default.builtinModules, ], plugins: [stubNativePlugin], treeShaking: true, sourcemap: false, logLevel: 'warning', logOverride: { 'indirect-require': 'silent', }, }); if (result.errors.length > 0) { console.error('❌ 构建错误:'); result.errors.forEach(e => console.error(e)); process.exit(1); } if (result.warnings.length > 0) { console.warn(`⚠️ ${result.warnings.length} 个警告(忽略)`); } const stat = fs.statSync(OUTPUT_FILE); const sizeMB = (stat.size / 1024 / 1024).toFixed(2); const elapsed = ((Date.now() - t0) / 1000).toFixed(2); console.log(`✅ Bundle 完成: ${OUTPUT_FILE}`); console.log(` 大小: ${sizeMB} MB`); console.log(` 耗时: ${elapsed}s`); // --- 同步 extensions --- console.log('\n--- 同步 extensions ---'); const feishuSrc = path.join(OPENCLAW_ROOT, 'extensions/feishu'); const feishuDst = path.join(OUTPUT_DIR, 'extensions/feishu'); if (fs.existsSync(feishuSrc)) { const { execSync } = await import('node:child_process'); fs.mkdirSync(path.dirname(feishuDst), { recursive: true }); execSync( `rsync -a --delete --exclude='.esbuild-cache' --exclude='*.test.*' --exclude='node_modules' "${feishuSrc}/" "${feishuDst}/"`, { stdio: 'inherit' }, ); console.log('✅ feishu extension 已同步'); } // --- Bundle plugin-sdk(给 feishu 编译时 alias 用)--- console.log('\n--- Bundle plugin-sdk ---'); const pluginSdkEntry = (() => { // src 版本比 dist 更完整(dist 可能缺少部分 re-export) const src = path.join(OPENCLAW_ROOT, 'src/plugin-sdk/index.ts'); const dist = path.join(OPENCLAW_ROOT, 'dist/plugin-sdk/index.js'); return fs.existsSync(src) ? src : dist; })(); const pluginSdkAccountIdEntry = (() => { const src = path.join(OPENCLAW_ROOT, 'src/plugin-sdk/account-id.ts'); const dist = path.join(OPENCLAW_ROOT, 'dist/plugin-sdk/account-id.js'); return fs.existsSync(src) ? src : dist; })(); const pluginSdkOutDir = path.join(OUTPUT_DIR, 'plugin-sdk'); fs.mkdirSync(pluginSdkOutDir, { recursive: true }); if (fs.existsSync(pluginSdkEntry)) { await esbuild.build({ entryPoints: [pluginSdkEntry], bundle: true, format: 'esm', platform: 'node', target: 'node18', outfile: path.join(pluginSdkOutDir, 'index.mjs'), banner: { js: `import { createRequire as __bundleCreateRequire } from 'node:module'; const require = __bundleCreateRequire(import.meta.url);`, }, external: ['node:*', ...(await import('node:module')).default.builtinModules], plugins: [stubNativePlugin], treeShaking: true, sourcemap: false, logLevel: 'warning', logOverride: { 'indirect-require': 'silent' }, }); console.log(`✅ plugin-sdk/index.mjs bundle 完成`); } if (fs.existsSync(pluginSdkAccountIdEntry)) { await esbuild.build({ entryPoints: [pluginSdkAccountIdEntry], bundle: true, format: 'esm', platform: 'node', target: 'node18', outfile: path.join(pluginSdkOutDir, 'account-id.mjs'), banner: { js: `import { createRequire as __bundleCreateRequire } from 'node:module'; const require = __bundleCreateRequire(import.meta.url);`, }, external: ['node:*', ...(await import('node:module')).default.builtinModules], plugins: [stubNativePlugin], treeShaking: true, sourcemap: false, logLevel: 'warning', logOverride: { 'indirect-require': 'silent' }, }); console.log(`✅ plugin-sdk/account-id.mjs bundle 完成`); } // --- 写 package.json --- const pkgJson = { name: 'openclaw-dist', private: true, type: 'module', description: 'Bundled OpenClaw core modules for multi-agent-system', }; fs.writeFileSync( path.join(OUTPUT_DIR, 'package.json'), JSON.stringify(pkgJson, null, 2) + '\n', ); console.log('✅ package.json 已写入'); console.log('\n=== 完成 ==='); console.log(`产出目录: ${OUTPUT_DIR}`); } main().catch(err => { console.error('❌', err.message || err); process.exit(1); });