mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 20:33:59 +02:00
286 lines
8.9 KiB
TypeScript
286 lines
8.9 KiB
TypeScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { collectBundledChannelConfigs } from "./bundled-channel-config-metadata.js";
|
|
import {
|
|
collectBundledPluginPublicSurfaceArtifacts,
|
|
collectBundledPluginRuntimeSidecarArtifacts,
|
|
deriveBundledPluginIdHint,
|
|
resolveBundledPluginScanDir,
|
|
} from "./bundled-plugin-scan.js";
|
|
import {
|
|
getPackageManifestMetadata,
|
|
loadPluginManifest,
|
|
type OpenClawPackageManifest,
|
|
type PackageManifest,
|
|
type PluginManifest,
|
|
} from "./manifest.js";
|
|
import { resolveLoaderPackageRoot } from "./sdk-alias.js";
|
|
|
|
const OPENCLAW_PACKAGE_ROOT =
|
|
resolveLoaderPackageRoot({
|
|
modulePath: fileURLToPath(import.meta.url),
|
|
moduleUrl: import.meta.url,
|
|
}) ?? fileURLToPath(new URL("../..", import.meta.url));
|
|
const CURRENT_MODULE_PATH = fileURLToPath(import.meta.url);
|
|
const RUNNING_FROM_BUILT_ARTIFACT =
|
|
CURRENT_MODULE_PATH.includes(`${path.sep}dist${path.sep}`) ||
|
|
CURRENT_MODULE_PATH.includes(`${path.sep}dist-runtime${path.sep}`);
|
|
|
|
type BundledPluginPathPair = {
|
|
source: string;
|
|
built: string;
|
|
};
|
|
|
|
export type BundledPluginMetadata = {
|
|
dirName: string;
|
|
idHint: string;
|
|
source: BundledPluginPathPair;
|
|
setupSource?: BundledPluginPathPair;
|
|
publicSurfaceArtifacts?: readonly string[];
|
|
runtimeSidecarArtifacts?: readonly string[];
|
|
packageName?: string;
|
|
packageVersion?: string;
|
|
packageDescription?: string;
|
|
packageManifest?: OpenClawPackageManifest;
|
|
manifest: PluginManifest;
|
|
};
|
|
|
|
const bundledPluginMetadataCache = new Map<string, readonly BundledPluginMetadata[]>();
|
|
|
|
export function clearBundledPluginMetadataCache(): void {
|
|
bundledPluginMetadataCache.clear();
|
|
}
|
|
|
|
function trimString(value: unknown): string | undefined {
|
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
}
|
|
|
|
function normalizeStringList(value: unknown): string[] {
|
|
if (!Array.isArray(value)) {
|
|
return [];
|
|
}
|
|
return value.map((entry) => trimString(entry) ?? "").filter(Boolean);
|
|
}
|
|
|
|
function rewriteEntryToBuiltPath(entry: string | undefined): string | undefined {
|
|
if (!entry) {
|
|
return undefined;
|
|
}
|
|
const normalized = entry.replace(/^\.\//u, "");
|
|
return normalized.replace(/\.[^.]+$/u, ".js");
|
|
}
|
|
|
|
function readPackageManifest(pluginDir: string): PackageManifest | undefined {
|
|
const packagePath = path.join(pluginDir, "package.json");
|
|
if (!fs.existsSync(packagePath)) {
|
|
return undefined;
|
|
}
|
|
try {
|
|
return JSON.parse(fs.readFileSync(packagePath, "utf-8")) as PackageManifest;
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
function collectBundledPluginMetadataForPackageRoot(
|
|
packageRoot: string,
|
|
includeChannelConfigs: boolean,
|
|
includeSyntheticChannelConfigs: boolean,
|
|
): readonly BundledPluginMetadata[] {
|
|
const scanDir = resolveBundledPluginScanDir({
|
|
packageRoot,
|
|
runningFromBuiltArtifact: RUNNING_FROM_BUILT_ARTIFACT,
|
|
});
|
|
if (!scanDir || !fs.existsSync(scanDir)) {
|
|
return [];
|
|
}
|
|
|
|
const entries: BundledPluginMetadata[] = [];
|
|
for (const dirName of fs
|
|
.readdirSync(scanDir, { withFileTypes: true })
|
|
.filter((entry) => entry.isDirectory())
|
|
.map((entry) => entry.name)
|
|
.toSorted((left, right) => left.localeCompare(right))) {
|
|
const pluginDir = path.join(scanDir, dirName);
|
|
const manifestResult = loadPluginManifest(pluginDir, false);
|
|
if (!manifestResult.ok) {
|
|
continue;
|
|
}
|
|
|
|
const packageJson = readPackageManifest(pluginDir);
|
|
const packageManifest = getPackageManifestMetadata(packageJson);
|
|
const extensions = normalizeStringList(packageManifest?.extensions);
|
|
if (extensions.length === 0) {
|
|
continue;
|
|
}
|
|
const sourceEntry = trimString(extensions[0]);
|
|
const builtEntry = rewriteEntryToBuiltPath(sourceEntry);
|
|
if (!sourceEntry || !builtEntry) {
|
|
continue;
|
|
}
|
|
|
|
const setupSourcePath = trimString(packageManifest?.setupEntry);
|
|
const setupSource =
|
|
setupSourcePath && rewriteEntryToBuiltPath(setupSourcePath)
|
|
? {
|
|
source: setupSourcePath,
|
|
built: rewriteEntryToBuiltPath(setupSourcePath)!,
|
|
}
|
|
: undefined;
|
|
const publicSurfaceArtifacts = collectBundledPluginPublicSurfaceArtifacts({
|
|
pluginDir,
|
|
sourceEntry,
|
|
...(setupSourcePath ? { setupEntry: setupSourcePath } : {}),
|
|
});
|
|
const runtimeSidecarArtifacts =
|
|
collectBundledPluginRuntimeSidecarArtifacts(publicSurfaceArtifacts);
|
|
const channelConfigs =
|
|
includeChannelConfigs && includeSyntheticChannelConfigs
|
|
? collectBundledChannelConfigs({
|
|
pluginDir,
|
|
manifest: manifestResult.manifest,
|
|
packageManifest,
|
|
})
|
|
: manifestResult.manifest.channelConfigs;
|
|
|
|
entries.push({
|
|
dirName,
|
|
idHint: deriveBundledPluginIdHint({
|
|
entryPath: sourceEntry,
|
|
manifestId: manifestResult.manifest.id,
|
|
packageName: trimString(packageJson?.name),
|
|
hasMultipleExtensions: extensions.length > 1,
|
|
}),
|
|
source: {
|
|
source: sourceEntry,
|
|
built: builtEntry,
|
|
},
|
|
...(setupSource ? { setupSource } : {}),
|
|
...(publicSurfaceArtifacts ? { publicSurfaceArtifacts } : {}),
|
|
...(runtimeSidecarArtifacts ? { runtimeSidecarArtifacts } : {}),
|
|
...(trimString(packageJson?.name) ? { packageName: trimString(packageJson?.name) } : {}),
|
|
...(trimString(packageJson?.version)
|
|
? { packageVersion: trimString(packageJson?.version) }
|
|
: {}),
|
|
...(trimString(packageJson?.description)
|
|
? { packageDescription: trimString(packageJson?.description) }
|
|
: {}),
|
|
...(packageManifest ? { packageManifest } : {}),
|
|
manifest: {
|
|
...manifestResult.manifest,
|
|
...(channelConfigs ? { channelConfigs } : {}),
|
|
},
|
|
});
|
|
}
|
|
|
|
return entries;
|
|
}
|
|
|
|
export function listBundledPluginMetadata(params?: {
|
|
rootDir?: string;
|
|
includeChannelConfigs?: boolean;
|
|
includeSyntheticChannelConfigs?: boolean;
|
|
}): readonly BundledPluginMetadata[] {
|
|
const rootDir = path.resolve(params?.rootDir ?? OPENCLAW_PACKAGE_ROOT);
|
|
const includeChannelConfigs = params?.includeChannelConfigs ?? !RUNNING_FROM_BUILT_ARTIFACT;
|
|
const includeSyntheticChannelConfigs =
|
|
params?.includeSyntheticChannelConfigs ?? includeChannelConfigs;
|
|
const cacheKey = JSON.stringify({
|
|
rootDir,
|
|
includeChannelConfigs,
|
|
includeSyntheticChannelConfigs,
|
|
});
|
|
const cached = bundledPluginMetadataCache.get(cacheKey);
|
|
if (cached) {
|
|
return cached;
|
|
}
|
|
const entries = Object.freeze(
|
|
collectBundledPluginMetadataForPackageRoot(
|
|
rootDir,
|
|
includeChannelConfigs,
|
|
includeSyntheticChannelConfigs,
|
|
),
|
|
);
|
|
bundledPluginMetadataCache.set(cacheKey, entries);
|
|
return entries;
|
|
}
|
|
|
|
export function findBundledPluginMetadataById(
|
|
pluginId: string,
|
|
params?: { rootDir?: string },
|
|
): BundledPluginMetadata | undefined {
|
|
return listBundledPluginMetadata(params).find((entry) => entry.manifest.id === pluginId);
|
|
}
|
|
|
|
export function resolveBundledPluginWorkspaceSourcePath(params: {
|
|
rootDir: string;
|
|
pluginId: string;
|
|
}): string | null {
|
|
const metadata = findBundledPluginMetadataById(params.pluginId, { rootDir: params.rootDir });
|
|
if (!metadata) {
|
|
return null;
|
|
}
|
|
return path.resolve(params.rootDir, "extensions", metadata.dirName);
|
|
}
|
|
|
|
export function resolveBundledPluginGeneratedPath(
|
|
rootDir: string,
|
|
entry: BundledPluginPathPair | undefined,
|
|
pluginDirName?: string,
|
|
): string | null {
|
|
if (!entry) {
|
|
return null;
|
|
}
|
|
const entryOrder = [entry.built, entry.source].filter(
|
|
(candidate): candidate is string => typeof candidate === "string" && candidate.length > 0,
|
|
);
|
|
const baseDirs = [
|
|
path.resolve(rootDir, "dist", "extensions", pluginDirName ?? ""),
|
|
path.resolve(rootDir, "extensions", pluginDirName ?? ""),
|
|
];
|
|
for (const baseDir of baseDirs) {
|
|
for (const entryPath of entryOrder) {
|
|
const candidate = path.resolve(baseDir, normalizeRelativePluginEntryPath(entryPath));
|
|
if (fs.existsSync(candidate)) {
|
|
return candidate;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function normalizeRelativePluginEntryPath(entryPath: string): string {
|
|
return entryPath.replace(/^\.\//u, "");
|
|
}
|
|
|
|
export function resolveBundledPluginRepoEntryPath(params: {
|
|
rootDir: string;
|
|
pluginId: string;
|
|
preferBuilt?: boolean;
|
|
}): string | null {
|
|
const metadata = findBundledPluginMetadataById(params.pluginId, { rootDir: params.rootDir });
|
|
if (!metadata) {
|
|
return null;
|
|
}
|
|
|
|
const entryOrder = params.preferBuilt
|
|
? [metadata.source.built, metadata.source.source]
|
|
: [metadata.source.source, metadata.source.built];
|
|
const baseDirs = [
|
|
path.resolve(params.rootDir, "dist", "extensions", metadata.dirName),
|
|
path.resolve(params.rootDir, "extensions", metadata.dirName),
|
|
];
|
|
|
|
for (const baseDir of baseDirs) {
|
|
for (const entryPath of entryOrder) {
|
|
const candidate = path.resolve(baseDir, normalizeRelativePluginEntryPath(entryPath));
|
|
if (fs.existsSync(candidate)) {
|
|
return candidate;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|