fix: openclaw module loading (#993)

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Haze <hazeone@users.noreply.github.com>
This commit is contained in:
Haze
2026-05-08 15:36:39 +08:00
committed by GitHub
parent c6adf36b57
commit 15e8fe8e05
4 changed files with 240 additions and 3 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "clawx",
"version": "0.4.1",
"version": "0.4.2-alpha.0",
"pnpm": {
"onlyBuiltDependencies": [
"@discordjs/opus",
@@ -180,5 +180,5 @@
"zustand": "^5.0.11",
"zx": "^8.8.5"
},
"packageManager": "pnpm@10.31.0+sha512.e3927388bfaa8078ceb79b748ffc1e8274e84d75163e67bc22e06c0d3aed43dd153151cbf11d7f8301ff4acb98c68bdc5cadf6989532801ffafe3b3e4a63c268"
}
"packageManager": "pnpm@10.33.4+sha512.1c67b3b359b2d408119ba1ed289f34b8fc3c6873412bec6fd264fbdc82489e510fcbecb9ce9d22dae7f3b76269d8441046014bdca53b9979cd7a561ad631b800"
}

View File

@@ -18,6 +18,7 @@
import 'zx/globals';
import { EXTRA_BUNDLED_PACKAGES } from './openclaw-bundle-config.mjs';
import { patchExtensionOpenClawSelfImports } from './openclaw-self-import-patch.mjs';
const ROOT = path.resolve(__dirname, '..');
const OUTPUT = path.join(ROOT, 'build', 'openclaw');
@@ -973,6 +974,11 @@ function patchBundledRuntime(outputDir) {
patchBrokenModules(outputNodeModules);
patchBundledRuntime(OUTPUT);
const openclawSelfImportPatch = patchExtensionOpenClawSelfImports(OUTPUT);
if (openclawSelfImportPatch.specifiersPatched > 0) {
echo` 🩹 Rewrote ${openclawSelfImportPatch.specifiersPatched} OpenClaw plugin-sdk self-import(s) in ${openclawSelfImportPatch.filesPatched} extension file(s)`;
}
// 8. Verify the bundle
const entryExists = fs.existsSync(path.join(OUTPUT, 'openclaw.mjs'));
const distExists = fs.existsSync(path.join(OUTPUT, 'dist', 'entry.js'));

View File

@@ -0,0 +1,127 @@
import fs from 'node:fs';
import path from 'node:path';
export const OPENCLAW_PLUGIN_SDK_PREFIX = 'openclaw/plugin-sdk/';
const OPENCLAW_PLUGIN_SDK_SPECIFIER_RE = /(["'])openclaw\/plugin-sdk\/([^"'\r\n]+)\1/g;
function assertSafePluginSdkSubpath(subpath) {
if (!subpath || subpath.startsWith('/') || subpath.startsWith('\\')) {
throw new Error(`Invalid OpenClaw plugin-sdk import subpath: ${JSON.stringify(subpath)}`);
}
const parts = subpath.split(/[\\/]+/);
if (parts.some((part) => !part || part === '.' || part === '..')) {
throw new Error(`Invalid OpenClaw plugin-sdk import subpath: ${JSON.stringify(subpath)}`);
}
}
export function toImportSpecifier(relativePath) {
const normalized = relativePath.split(path.sep).join('/');
if (normalized.startsWith('./') || normalized.startsWith('../')) {
return normalized;
}
return `./${normalized}`;
}
export function resolvePluginSdkTarget(distDir, subpath) {
assertSafePluginSdkSubpath(subpath);
const targetSubpath = subpath.endsWith('.js') ? subpath : `${subpath}.js`;
return path.join(distDir, 'plugin-sdk', ...targetSubpath.split(/[\\/]+/));
}
export function rewriteOpenClawPluginSdkSpecifiers(content, options) {
const { filePath, distDir } = options;
let replacements = 0;
const nextContent = content.replace(
OPENCLAW_PLUGIN_SDK_SPECIFIER_RE,
(match, quote, subpath) => {
const target = resolvePluginSdkTarget(distDir, subpath);
if (!fs.existsSync(target)) {
throw new Error(
`Cannot rewrite ${match} in ${filePath}: missing bundled SDK target ${target}`,
);
}
replacements++;
const relativePath = path.relative(path.dirname(filePath), target);
return `${quote}${toImportSpecifier(relativePath)}${quote}`;
},
);
return {
content: nextContent,
replacements,
};
}
function listJavaScriptFiles(rootDir) {
const files = [];
const stack = [rootDir];
while (stack.length > 0) {
const current = stack.pop();
let entries;
try {
entries = fs.readdirSync(current, { withFileTypes: true });
} catch {
continue;
}
for (const entry of entries) {
const fullPath = path.join(current, entry.name);
if (entry.isDirectory()) {
stack.push(fullPath);
continue;
}
if (entry.isFile() && entry.name.endsWith('.js')) {
files.push(fullPath);
}
}
}
return files;
}
export function patchExtensionOpenClawSelfImports(outputDir) {
const distDir = path.join(outputDir, 'dist');
const extensionsDir = path.join(distDir, 'extensions');
if (!fs.existsSync(extensionsDir)) {
return {
filesScanned: 0,
filesPatched: 0,
specifiersPatched: 0,
};
}
let filesScanned = 0;
let filesPatched = 0;
let specifiersPatched = 0;
for (const filePath of listJavaScriptFiles(extensionsDir)) {
filesScanned++;
const content = fs.readFileSync(filePath, 'utf8');
if (!content.includes(OPENCLAW_PLUGIN_SDK_PREFIX)) {
continue;
}
const result = rewriteOpenClawPluginSdkSpecifiers(content, {
filePath,
distDir,
});
if (result.replacements > 0 && result.content !== content) {
fs.writeFileSync(filePath, result.content, 'utf8');
filesPatched++;
specifiersPatched += result.replacements;
}
}
return {
filesScanned,
filesPatched,
specifiersPatched,
};
}

View File

@@ -0,0 +1,104 @@
// @vitest-environment node
import { afterEach, describe, expect, it } from 'vitest';
import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import {
patchExtensionOpenClawSelfImports,
rewriteOpenClawPluginSdkSpecifiers,
toImportSpecifier,
} from '../../scripts/openclaw-self-import-patch.mjs';
const tempRoots: string[] = [];
async function createTempOpenClawBundle(): Promise<string> {
const root = await mkdtemp(path.join(tmpdir(), 'clawx-openclaw-self-import-'));
tempRoots.push(root);
return root;
}
afterEach(async () => {
await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
});
describe('openclaw self-import bundle patch', () => {
it('converts OpenClaw plugin-sdk package specifiers to bundled relative paths', async () => {
const root = await createTempOpenClawBundle();
const distDir = path.join(root, 'dist');
const pluginSdkDir = path.join(distDir, 'plugin-sdk');
const extensionDir = path.join(distDir, 'extensions', 'codex');
await mkdir(pluginSdkDir, { recursive: true });
await mkdir(extensionDir, { recursive: true });
await writeFile(path.join(pluginSdkDir, 'provider-model-shared.js'), 'export const ok = true;\n');
const promptOverlayPath = path.join(extensionDir, 'prompt-overlay.js');
await writeFile(
promptOverlayPath,
[
'import { ok } from "openclaw/plugin-sdk/provider-model-shared";',
"export { ok };",
'',
].join('\n'),
);
const result = patchExtensionOpenClawSelfImports(root);
expect(result).toMatchObject({
filesPatched: 1,
specifiersPatched: 1,
});
await expect(readFile(promptOverlayPath, 'utf8')).resolves.toContain(
'from "../../plugin-sdk/provider-model-shared.js"',
);
});
it('leaves extension files without OpenClaw self-imports untouched', async () => {
const root = await createTempOpenClawBundle();
const extensionDir = path.join(root, 'dist', 'extensions', 'telegram');
await mkdir(extensionDir, { recursive: true });
const filePath = path.join(extensionDir, 'runtime.js');
const source = 'export const runtime = true;\n';
await writeFile(filePath, source);
const result = patchExtensionOpenClawSelfImports(root);
expect(result.filesScanned).toBe(1);
expect(result.filesPatched).toBe(0);
expect(result.specifiersPatched).toBe(0);
await expect(readFile(filePath, 'utf8')).resolves.toBe(source);
});
it('throws when the bundled plugin-sdk target is missing', () => {
const root = path.join(tmpdir(), 'missing-target-openclaw-bundle');
const distDir = path.join(root, 'dist');
const filePath = path.join(distDir, 'extensions', 'codex', 'prompt-overlay.js');
expect(() => rewriteOpenClawPluginSdkSpecifiers(
'import "openclaw/plugin-sdk/provider-model-shared";',
{ filePath, distDir },
)).toThrow(/missing bundled SDK target/);
});
it('formats same-directory import paths with an explicit relative prefix', () => {
expect(toImportSpecifier('provider-model-shared.js')).toBe('./provider-model-shared.js');
expect(toImportSpecifier('../plugin-sdk/provider-model-shared.js')).toBe(
'../plugin-sdk/provider-model-shared.js',
);
});
it('returns an empty patch summary when the extensions directory is absent', async () => {
const root = await createTempOpenClawBundle();
await mkdir(path.join(root, 'dist', 'plugin-sdk'), { recursive: true });
const result = patchExtensionOpenClawSelfImports(root);
expect(existsSync(path.join(root, 'dist', 'extensions'))).toBe(false);
expect(result).toEqual({
filesScanned: 0,
filesPatched: 0,
specifiersPatched: 0,
});
});
});