feat(channels): add DingTalk via bundled plugin mirror and pure Node deploy (#215)
This commit is contained in:
committed by
GitHub
Unverified
parent
d4f77a442c
commit
d63810f54b
@@ -149,6 +149,8 @@ exports.default = async function afterPack(context) {
|
||||
|
||||
const openclawRoot = join(resourcesDir, 'openclaw');
|
||||
const dest = join(openclawRoot, 'node_modules');
|
||||
const pluginsSrcRoot = join(__dirname, '..', 'build', 'openclaw-plugins');
|
||||
const pluginsDestRoot = join(resourcesDir, 'openclaw-plugins');
|
||||
|
||||
if (!existsSync(src)) {
|
||||
console.warn('[after-pack] ⚠️ build/openclaw/node_modules not found. Run bundle-openclaw first.');
|
||||
@@ -164,6 +166,29 @@ exports.default = async function afterPack(context) {
|
||||
cpSync(src, dest, { recursive: true });
|
||||
console.log('[after-pack] ✅ openclaw node_modules copied.');
|
||||
|
||||
// 1.1 Copy plugin node_modules (also skipped due to .gitignore)
|
||||
if (existsSync(pluginsSrcRoot) && existsSync(pluginsDestRoot)) {
|
||||
const pluginDirs = readdirSync(pluginsSrcRoot, { withFileTypes: true })
|
||||
.filter((d) => d.isDirectory())
|
||||
.map((d) => d.name);
|
||||
|
||||
for (const pluginId of pluginDirs) {
|
||||
const pluginSrcNM = join(pluginsSrcRoot, pluginId, 'node_modules');
|
||||
const pluginDestRoot = join(pluginsDestRoot, pluginId);
|
||||
const pluginDestNM = join(pluginDestRoot, 'node_modules');
|
||||
if (!existsSync(pluginSrcNM) || !existsSync(pluginDestRoot)) continue;
|
||||
|
||||
console.log(`[after-pack] Copying plugin deps for ${pluginId} -> ${pluginDestNM}`);
|
||||
cpSync(pluginSrcNM, pluginDestNM, { recursive: true });
|
||||
|
||||
// Apply the same cleanup strategy for plugin bundles.
|
||||
cleanupUnnecessaryFiles(pluginDestRoot);
|
||||
cleanupKoffi(pluginDestNM, platform, arch);
|
||||
cleanupNativePlatformPackages(pluginDestNM, platform, arch);
|
||||
}
|
||||
console.log('[after-pack] ✅ openclaw plugin node_modules copied.');
|
||||
}
|
||||
|
||||
// 2. General cleanup on the full openclaw directory (not just node_modules)
|
||||
console.log('[after-pack] 🧹 Cleaning up unnecessary files ...');
|
||||
const removedRoot = cleanupUnnecessaryFiles(openclawRoot);
|
||||
|
||||
159
scripts/bundle-openclaw-plugins.mjs
Normal file
159
scripts/bundle-openclaw-plugins.mjs
Normal file
@@ -0,0 +1,159 @@
|
||||
#!/usr/bin/env zx
|
||||
|
||||
/**
|
||||
* bundle-openclaw-plugins.mjs
|
||||
*
|
||||
* Build a self-contained mirror of OpenClaw third-party plugins for packaging.
|
||||
* Current plugins:
|
||||
* - @soimy/dingtalk -> build/openclaw-plugins/dingtalk
|
||||
*
|
||||
* The output plugin directory contains:
|
||||
* - plugin source files (index.ts, openclaw.plugin.json, package.json, ...)
|
||||
* - plugin runtime node_modules/ (flattened direct + transitive deps)
|
||||
*/
|
||||
|
||||
import 'zx/globals';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const OUTPUT_ROOT = path.join(ROOT, 'build', 'openclaw-plugins');
|
||||
const NODE_MODULES = path.join(ROOT, 'node_modules');
|
||||
|
||||
const PLUGINS = [
|
||||
{ npmName: '@soimy/dingtalk', pluginId: 'dingtalk' },
|
||||
];
|
||||
|
||||
function getVirtualStoreNodeModules(realPkgPath) {
|
||||
let dir = realPkgPath;
|
||||
while (dir !== path.dirname(dir)) {
|
||||
if (path.basename(dir) === 'node_modules') return dir;
|
||||
dir = path.dirname(dir);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function listPackages(nodeModulesDir) {
|
||||
const result = [];
|
||||
if (!fs.existsSync(nodeModulesDir)) return result;
|
||||
|
||||
for (const entry of fs.readdirSync(nodeModulesDir)) {
|
||||
if (entry === '.bin') continue;
|
||||
const entryPath = path.join(nodeModulesDir, entry);
|
||||
const stat = fs.lstatSync(entryPath);
|
||||
|
||||
if (entry.startsWith('@')) {
|
||||
if (!(stat.isDirectory() || stat.isSymbolicLink())) continue;
|
||||
let scopeEntries = [];
|
||||
try {
|
||||
scopeEntries = fs.readdirSync(entryPath);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
for (const sub of scopeEntries) {
|
||||
result.push({
|
||||
name: `${entry}/${sub}`,
|
||||
fullPath: path.join(entryPath, sub),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
result.push({ name: entry, fullPath: entryPath });
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function bundleOnePlugin({ npmName, pluginId }) {
|
||||
const pkgPath = path.join(NODE_MODULES, ...npmName.split('/'));
|
||||
if (!fs.existsSync(pkgPath)) {
|
||||
throw new Error(`Missing dependency "${npmName}". Run pnpm install first.`);
|
||||
}
|
||||
|
||||
const realPluginPath = fs.realpathSync(pkgPath);
|
||||
const outputDir = path.join(OUTPUT_ROOT, pluginId);
|
||||
|
||||
echo`📦 Bundling plugin ${npmName} -> ${outputDir}`;
|
||||
|
||||
if (fs.existsSync(outputDir)) {
|
||||
fs.rmSync(outputDir, { recursive: true, force: true });
|
||||
}
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
|
||||
// 1) Copy plugin package itself
|
||||
fs.cpSync(realPluginPath, outputDir, { recursive: true, dereference: true });
|
||||
|
||||
// 2) Collect transitive deps from pnpm virtual store
|
||||
const collected = new Map();
|
||||
const queue = [];
|
||||
const rootVirtualNM = getVirtualStoreNodeModules(realPluginPath);
|
||||
if (!rootVirtualNM) {
|
||||
throw new Error(`Cannot resolve virtual store node_modules for ${npmName}`);
|
||||
}
|
||||
queue.push({ nodeModulesDir: rootVirtualNM, skipPkg: npmName });
|
||||
|
||||
const SKIP_PACKAGES = new Set(['typescript', '@playwright/test']);
|
||||
const SKIP_SCOPES = ['@types/'];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const { nodeModulesDir, skipPkg } = queue.shift();
|
||||
for (const { name, fullPath } of listPackages(nodeModulesDir)) {
|
||||
if (name === skipPkg) continue;
|
||||
if (SKIP_PACKAGES.has(name) || SKIP_SCOPES.some((s) => name.startsWith(s))) continue;
|
||||
|
||||
let realPath;
|
||||
try {
|
||||
realPath = fs.realpathSync(fullPath);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (collected.has(realPath)) continue;
|
||||
collected.set(realPath, name);
|
||||
|
||||
const depVirtualNM = getVirtualStoreNodeModules(realPath);
|
||||
if (depVirtualNM && depVirtualNM !== nodeModulesDir) {
|
||||
queue.push({ nodeModulesDir: depVirtualNM, skipPkg: name });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Copy flattened deps into plugin/node_modules
|
||||
const outputNodeModules = path.join(outputDir, 'node_modules');
|
||||
fs.mkdirSync(outputNodeModules, { recursive: true });
|
||||
|
||||
let copiedCount = 0;
|
||||
let skippedDupes = 0;
|
||||
const copiedNames = new Set();
|
||||
|
||||
for (const [realPath, pkgName] of collected) {
|
||||
if (copiedNames.has(pkgName)) {
|
||||
skippedDupes++;
|
||||
continue;
|
||||
}
|
||||
copiedNames.add(pkgName);
|
||||
|
||||
const dest = path.join(outputNodeModules, pkgName);
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
||||
fs.cpSync(realPath, dest, { recursive: true, dereference: true });
|
||||
copiedCount++;
|
||||
} catch (err) {
|
||||
echo` ⚠️ Skipped ${pkgName}: ${err.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
const manifestPath = path.join(outputDir, 'openclaw.plugin.json');
|
||||
if (!fs.existsSync(manifestPath)) {
|
||||
throw new Error(`Missing openclaw.plugin.json in bundled plugin output: ${pluginId}`);
|
||||
}
|
||||
|
||||
echo` ✅ ${pluginId}: copied ${copiedCount} deps (skipped dupes: ${skippedDupes})`;
|
||||
}
|
||||
|
||||
echo`📦 Bundling OpenClaw plugin mirrors...`;
|
||||
fs.mkdirSync(OUTPUT_ROOT, { recursive: true });
|
||||
|
||||
for (const plugin of PLUGINS) {
|
||||
bundleOnePlugin(plugin);
|
||||
}
|
||||
|
||||
echo`✅ Plugin mirrors ready: ${OUTPUT_ROOT}`;
|
||||
Reference in New Issue
Block a user