mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 09:41:11 +00:00
376 lines
12 KiB
TypeScript
376 lines
12 KiB
TypeScript
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { resolveOpenClawPackageRootSync } from "../../infra/openclaw-root.js";
|
|
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
|
import type {
|
|
BundledChannelEntryContract,
|
|
BundledChannelSetupEntryContract,
|
|
} from "../../plugin-sdk/channel-entry-contract.js";
|
|
import {
|
|
listBundledChannelPluginMetadata,
|
|
resolveBundledChannelGeneratedPath,
|
|
type BundledChannelPluginMetadata,
|
|
} from "../../plugins/bundled-channel-runtime.js";
|
|
import type { PluginRuntime } from "../../plugins/runtime/types.js";
|
|
import { isJavaScriptModulePath, loadChannelPluginModule } from "./module-loader.js";
|
|
import type { ChannelId, ChannelPlugin } from "./types.js";
|
|
|
|
type GeneratedBundledChannelEntry = {
|
|
id: string;
|
|
entry: BundledChannelEntryContract;
|
|
setupEntry?: BundledChannelSetupEntryContract;
|
|
};
|
|
|
|
const log = createSubsystemLogger("channels");
|
|
const OPENCLAW_PACKAGE_ROOT =
|
|
resolveOpenClawPackageRootSync({
|
|
argv1: process.argv[1],
|
|
cwd: process.cwd(),
|
|
moduleUrl: import.meta.url.startsWith("file:") ? import.meta.url : undefined,
|
|
}) ??
|
|
(import.meta.url.startsWith("file:")
|
|
? path.resolve(fileURLToPath(new URL("../../..", import.meta.url)))
|
|
: process.cwd());
|
|
|
|
function resolveChannelPluginModuleEntry(
|
|
moduleExport: unknown,
|
|
): BundledChannelEntryContract | null {
|
|
const resolved =
|
|
moduleExport &&
|
|
typeof moduleExport === "object" &&
|
|
"default" in (moduleExport as Record<string, unknown>)
|
|
? (moduleExport as { default: unknown }).default
|
|
: moduleExport;
|
|
if (!resolved || typeof resolved !== "object") {
|
|
return null;
|
|
}
|
|
const record = resolved as Partial<BundledChannelEntryContract>;
|
|
if (record.kind !== "bundled-channel-entry") {
|
|
return null;
|
|
}
|
|
if (
|
|
typeof record.id !== "string" ||
|
|
typeof record.name !== "string" ||
|
|
typeof record.description !== "string" ||
|
|
typeof record.register !== "function" ||
|
|
typeof record.loadChannelPlugin !== "function"
|
|
) {
|
|
return null;
|
|
}
|
|
return record as BundledChannelEntryContract;
|
|
}
|
|
|
|
function resolveChannelSetupModuleEntry(
|
|
moduleExport: unknown,
|
|
): BundledChannelSetupEntryContract | null {
|
|
const resolved =
|
|
moduleExport &&
|
|
typeof moduleExport === "object" &&
|
|
"default" in (moduleExport as Record<string, unknown>)
|
|
? (moduleExport as { default: unknown }).default
|
|
: moduleExport;
|
|
if (!resolved || typeof resolved !== "object") {
|
|
return null;
|
|
}
|
|
const record = resolved as Partial<BundledChannelSetupEntryContract>;
|
|
if (record.kind !== "bundled-channel-setup-entry") {
|
|
return null;
|
|
}
|
|
if (typeof record.loadSetupPlugin !== "function") {
|
|
return null;
|
|
}
|
|
return record as BundledChannelSetupEntryContract;
|
|
}
|
|
|
|
function resolveBundledChannelBoundaryRoot(params: {
|
|
metadata: BundledChannelPluginMetadata;
|
|
modulePath: string;
|
|
}): string {
|
|
const distRoot = path.resolve(
|
|
OPENCLAW_PACKAGE_ROOT,
|
|
"dist",
|
|
"extensions",
|
|
params.metadata.dirName,
|
|
);
|
|
if (params.modulePath === distRoot || params.modulePath.startsWith(`${distRoot}${path.sep}`)) {
|
|
return distRoot;
|
|
}
|
|
return path.resolve(OPENCLAW_PACKAGE_ROOT, "extensions", params.metadata.dirName);
|
|
}
|
|
|
|
function resolveGeneratedBundledChannelModulePath(params: {
|
|
metadata: BundledChannelPluginMetadata;
|
|
entry: BundledChannelPluginMetadata["source"] | BundledChannelPluginMetadata["setupSource"];
|
|
}): string | null {
|
|
if (!params.entry) {
|
|
return null;
|
|
}
|
|
const candidateRoots = [
|
|
path.resolve(OPENCLAW_PACKAGE_ROOT, "dist", "extensions", params.metadata.dirName),
|
|
path.resolve(OPENCLAW_PACKAGE_ROOT, "extensions", params.metadata.dirName),
|
|
];
|
|
for (const rootDir of candidateRoots) {
|
|
const resolved = resolveBundledChannelGeneratedPath(
|
|
rootDir,
|
|
params.entry,
|
|
params.metadata.dirName,
|
|
);
|
|
if (resolved) {
|
|
return resolved;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function loadGeneratedBundledChannelModule(params: {
|
|
metadata: BundledChannelPluginMetadata;
|
|
entry: BundledChannelPluginMetadata["source"] | BundledChannelPluginMetadata["setupSource"];
|
|
}): unknown {
|
|
const modulePath = resolveGeneratedBundledChannelModulePath(params);
|
|
if (!modulePath) {
|
|
throw new Error(`missing generated module for bundled channel ${params.metadata.manifest.id}`);
|
|
}
|
|
return loadChannelPluginModule({
|
|
modulePath,
|
|
rootDir: resolveBundledChannelBoundaryRoot({
|
|
metadata: params.metadata,
|
|
modulePath,
|
|
}),
|
|
boundaryRootDir: resolveBundledChannelBoundaryRoot({
|
|
metadata: params.metadata,
|
|
modulePath,
|
|
}),
|
|
shouldTryNativeRequire: (safePath) =>
|
|
safePath.includes(`${path.sep}dist${path.sep}`) && isJavaScriptModulePath(safePath),
|
|
});
|
|
}
|
|
|
|
function loadGeneratedBundledChannelEntries(): readonly GeneratedBundledChannelEntry[] {
|
|
const entries: GeneratedBundledChannelEntry[] = [];
|
|
|
|
for (const metadata of listBundledChannelPluginMetadata({
|
|
includeChannelConfigs: false,
|
|
includeSyntheticChannelConfigs: false,
|
|
})) {
|
|
if ((metadata.manifest.channels?.length ?? 0) === 0) {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
const entry = resolveChannelPluginModuleEntry(
|
|
loadGeneratedBundledChannelModule({
|
|
metadata,
|
|
entry: metadata.source,
|
|
}),
|
|
);
|
|
if (!entry) {
|
|
log.warn(
|
|
`[channels] bundled channel entry ${metadata.manifest.id} missing bundled-channel-entry contract; skipping`,
|
|
);
|
|
continue;
|
|
}
|
|
const setupEntry = metadata.setupSource
|
|
? resolveChannelSetupModuleEntry(
|
|
loadGeneratedBundledChannelModule({
|
|
metadata,
|
|
entry: metadata.setupSource,
|
|
}),
|
|
)
|
|
: null;
|
|
entries.push({
|
|
id: metadata.manifest.id,
|
|
entry,
|
|
...(setupEntry ? { setupEntry } : {}),
|
|
});
|
|
} catch (error) {
|
|
const detail = error instanceof Error ? error.message : String(error);
|
|
log.warn(`[channels] failed to load bundled channel ${metadata.manifest.id}: ${detail}`);
|
|
}
|
|
}
|
|
|
|
return entries;
|
|
}
|
|
|
|
type BundledChannelState = {
|
|
entries: readonly GeneratedBundledChannelEntry[];
|
|
entriesById: Map<ChannelId, BundledChannelEntryContract>;
|
|
setupEntriesById: Map<ChannelId, BundledChannelSetupEntryContract>;
|
|
sortedIds: readonly ChannelId[];
|
|
pluginsById: Map<ChannelId, ChannelPlugin>;
|
|
setupPluginsById: Map<ChannelId, ChannelPlugin>;
|
|
secretsById: Map<ChannelId, ChannelPlugin["secrets"] | null>;
|
|
setupSecretsById: Map<ChannelId, ChannelPlugin["secrets"] | null>;
|
|
runtimeSettersById: Map<ChannelId, NonNullable<BundledChannelEntryContract["setChannelRuntime"]>>;
|
|
};
|
|
|
|
const EMPTY_BUNDLED_CHANNEL_STATE: BundledChannelState = {
|
|
entries: [],
|
|
entriesById: new Map(),
|
|
setupEntriesById: new Map(),
|
|
sortedIds: [],
|
|
pluginsById: new Map(),
|
|
setupPluginsById: new Map(),
|
|
secretsById: new Map(),
|
|
setupSecretsById: new Map(),
|
|
runtimeSettersById: new Map(),
|
|
};
|
|
|
|
let cachedBundledChannelState: BundledChannelState | null = null;
|
|
let bundledChannelStateLoadInProgress = false;
|
|
const pluginLoadInProgressIds = new Set<ChannelId>();
|
|
const setupPluginLoadInProgressIds = new Set<ChannelId>();
|
|
|
|
function getBundledChannelState(): BundledChannelState {
|
|
if (cachedBundledChannelState) {
|
|
return cachedBundledChannelState;
|
|
}
|
|
if (bundledChannelStateLoadInProgress) {
|
|
return EMPTY_BUNDLED_CHANNEL_STATE;
|
|
}
|
|
bundledChannelStateLoadInProgress = true;
|
|
const entries = loadGeneratedBundledChannelEntries();
|
|
const entriesById = new Map<ChannelId, BundledChannelEntryContract>();
|
|
const setupEntriesById = new Map<ChannelId, BundledChannelSetupEntryContract>();
|
|
const runtimeSettersById = new Map<
|
|
ChannelId,
|
|
NonNullable<BundledChannelEntryContract["setChannelRuntime"]>
|
|
>();
|
|
for (const { entry } of entries) {
|
|
if (entriesById.has(entry.id)) {
|
|
throw new Error(`duplicate bundled channel plugin id: ${entry.id}`);
|
|
}
|
|
entriesById.set(entry.id, entry);
|
|
if (entry.setChannelRuntime) {
|
|
runtimeSettersById.set(entry.id, entry.setChannelRuntime);
|
|
}
|
|
}
|
|
for (const { id, setupEntry } of entries) {
|
|
if (setupEntry) {
|
|
setupEntriesById.set(id, setupEntry);
|
|
}
|
|
}
|
|
|
|
try {
|
|
cachedBundledChannelState = {
|
|
entries,
|
|
entriesById,
|
|
setupEntriesById,
|
|
sortedIds: [...entriesById.keys()].toSorted((left, right) => left.localeCompare(right)),
|
|
pluginsById: new Map(),
|
|
setupPluginsById: new Map(),
|
|
secretsById: new Map(),
|
|
setupSecretsById: new Map(),
|
|
runtimeSettersById,
|
|
};
|
|
return cachedBundledChannelState;
|
|
} finally {
|
|
bundledChannelStateLoadInProgress = false;
|
|
}
|
|
}
|
|
|
|
export function listBundledChannelPlugins(): readonly ChannelPlugin[] {
|
|
const state = getBundledChannelState();
|
|
return state.sortedIds.flatMap((id) => {
|
|
const plugin = getBundledChannelPlugin(id);
|
|
return plugin ? [plugin] : [];
|
|
});
|
|
}
|
|
|
|
export function listBundledChannelSetupPlugins(): readonly ChannelPlugin[] {
|
|
const state = getBundledChannelState();
|
|
return state.sortedIds.flatMap((id) => {
|
|
const plugin = getBundledChannelSetupPlugin(id);
|
|
return plugin ? [plugin] : [];
|
|
});
|
|
}
|
|
|
|
export function getBundledChannelPlugin(id: ChannelId): ChannelPlugin | undefined {
|
|
const state = getBundledChannelState();
|
|
const cached = state.pluginsById.get(id);
|
|
if (cached) {
|
|
return cached;
|
|
}
|
|
if (pluginLoadInProgressIds.has(id)) {
|
|
return undefined;
|
|
}
|
|
const entry = state.entriesById.get(id);
|
|
if (!entry) {
|
|
return undefined;
|
|
}
|
|
pluginLoadInProgressIds.add(id);
|
|
try {
|
|
const plugin = entry.loadChannelPlugin();
|
|
state.pluginsById.set(id, plugin);
|
|
return plugin;
|
|
} finally {
|
|
pluginLoadInProgressIds.delete(id);
|
|
}
|
|
}
|
|
|
|
export function getBundledChannelSecrets(id: ChannelId): ChannelPlugin["secrets"] | undefined {
|
|
const state = getBundledChannelState();
|
|
if (state.secretsById.has(id)) {
|
|
return state.secretsById.get(id) ?? undefined;
|
|
}
|
|
const entry = state.entriesById.get(id);
|
|
if (!entry) {
|
|
return undefined;
|
|
}
|
|
const secrets = entry.loadChannelSecrets?.() ?? getBundledChannelPlugin(id)?.secrets;
|
|
state.secretsById.set(id, secrets ?? null);
|
|
return secrets;
|
|
}
|
|
|
|
export function getBundledChannelSetupPlugin(id: ChannelId): ChannelPlugin | undefined {
|
|
const state = getBundledChannelState();
|
|
const cached = state.setupPluginsById.get(id);
|
|
if (cached) {
|
|
return cached;
|
|
}
|
|
if (setupPluginLoadInProgressIds.has(id)) {
|
|
return undefined;
|
|
}
|
|
const entry = state.setupEntriesById.get(id);
|
|
if (!entry) {
|
|
return undefined;
|
|
}
|
|
setupPluginLoadInProgressIds.add(id);
|
|
try {
|
|
const plugin = entry.loadSetupPlugin();
|
|
state.setupPluginsById.set(id, plugin);
|
|
return plugin;
|
|
} finally {
|
|
setupPluginLoadInProgressIds.delete(id);
|
|
}
|
|
}
|
|
|
|
export function getBundledChannelSetupSecrets(id: ChannelId): ChannelPlugin["secrets"] | undefined {
|
|
const state = getBundledChannelState();
|
|
if (state.setupSecretsById.has(id)) {
|
|
return state.setupSecretsById.get(id) ?? undefined;
|
|
}
|
|
const entry = state.setupEntriesById.get(id);
|
|
if (!entry) {
|
|
return undefined;
|
|
}
|
|
const secrets = entry.loadSetupSecrets?.() ?? getBundledChannelSetupPlugin(id)?.secrets;
|
|
state.setupSecretsById.set(id, secrets ?? null);
|
|
return secrets;
|
|
}
|
|
|
|
export function requireBundledChannelPlugin(id: ChannelId): ChannelPlugin {
|
|
const plugin = getBundledChannelPlugin(id);
|
|
if (!plugin) {
|
|
throw new Error(`missing bundled channel plugin: ${id}`);
|
|
}
|
|
return plugin;
|
|
}
|
|
|
|
export function setBundledChannelRuntime(id: ChannelId, runtime: PluginRuntime): void {
|
|
const setter = getBundledChannelState().runtimeSettersById.get(id);
|
|
if (!setter) {
|
|
throw new Error(`missing bundled channel runtime setter: ${id}`);
|
|
}
|
|
setter(runtime);
|
|
}
|