mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
refactor(plugins): reuse scoped loader for capability discovery (#117373)
* refactor(plugins): reuse scoped loader for capabilities * fix(plugins): retain channel capabilities during discovery
This commit is contained in:
committed by
GitHub
parent
ea2e7f46a6
commit
fdec6fe7d4
@@ -25,9 +25,9 @@ ad60ccc4fe9084d47f0477e02d9296bacad32f26d7456e2a84be8d25a53a25c2 module/boolean
|
||||
16dc9d32e8ca3ef78fc63e0fc4b20e6b943633e9572de1a49d24a880b6ffc66c module/channel-config-primitives
|
||||
c0f910ebfa3dbf283145fb1e3b9c016d03e853ef13e70402b09f3b9d9c2f4ab0 module/channel-config-schema
|
||||
2dd98659d9600e755f09ef00dd91c36692b8562ed3700461937e94f6cd1e640a module/channel-contract
|
||||
23fba2ab304d27b33fb8923e542741a03dd93b5b60cffc0337d89c46090ace95 module/channel-core
|
||||
6e7bb72d2fe65cd2541af6c057dba4faa5079f410f34e08fcb5348ab38e7a666 module/channel-core
|
||||
9a5aaf650f9242523bb57bdc2556c323ab64e55aa11e25e2685b73b23ee12534 module/channel-dm-policy
|
||||
fbf353eb38ae68d8ded3f2a60b432c7bb2c245d2ec7e7c9f53c6da19a0db0938 module/channel-entry-contract
|
||||
ba41c40956d6b4565605fa38c2d12f4b8471a0f8afe842798716b9032ee4d74a module/channel-entry-contract
|
||||
982f29a18e07228e3da82cae67d06ff38249592a29c2fd28f01f0d2016ff80d9 module/channel-feedback
|
||||
d645d24bcb7a5f68cc46c692ad0d1fbd19be0a99996f31e9479ce9cffce301c1 module/channel-inbound
|
||||
76bb7f531f3702c801e8fe7479e9e499f601fb361a4303afdcb45fc0da440e4b module/channel-inbound-debounce
|
||||
@@ -58,7 +58,7 @@ eb4c757fe0086c1dbfa4c3f3caf3dcff0d3cab3924c608237f08f740a6ee5f59 module/command
|
||||
20f3f8042de53e4eee61b64de9102c8c202b9299e6a29235647a4729f70145f2 module/config-mutation
|
||||
189fa5a240cad0404cd281ad0a14a105a8f3231278d87b71cfbc4f96fb8e48ef module/config-runtime
|
||||
c1ea9510dfda047609a99d5d2cd1f1560f5d469a36e6b695766213d695c25b0f module/conversation-runtime
|
||||
8994adeb5aec2799c1a6d492f4f69db38ab15381038525666463580f520c5cfc module/core
|
||||
dea96213010cbc816345b1229534f1acb8b0bcfdbd7814c62eefc77030fa9cd8 module/core
|
||||
4af19d59c2f18674e7d7f7dc1b358b644dc707e6bd601dc47168bd9e4a669940 module/dedupe-runtime
|
||||
f70c93d28053ca2e8353e45e6515ce7acef188097c6117d1545965d0699c8004 module/device-bootstrap
|
||||
6215d3af5923bf5a616d73062534968b69f448e3e30adc64ae9caebdd1a46d71 module/diagnostic-runtime
|
||||
|
||||
@@ -170,7 +170,8 @@ export default definePluginEntry({
|
||||
|
||||
Wraps `definePluginEntry` with channel-specific wiring: it automatically
|
||||
calls `api.registerChannel({ plugin })`, exposes an optional root-help CLI
|
||||
metadata seam, and gates `registerFull` on registration mode.
|
||||
metadata seam, and gates capability and full-runtime callbacks on registration
|
||||
mode.
|
||||
|
||||
```typescript
|
||||
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/channel-core";
|
||||
@@ -187,19 +188,23 @@ export default defineChannelPluginEntry({
|
||||
registerFull(api) {
|
||||
api.registerGatewayMethod(/* ... */);
|
||||
},
|
||||
registerCapabilities(api) {
|
||||
api.registerTranscriptSourceProvider(/* ... */);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
| Field | Type | Required | Default |
|
||||
| --------------------- | ---------------------------------------------------------------- | -------- | ------------------- |
|
||||
| `id` | `string` | Yes | - |
|
||||
| `name` | `string` | Yes | - |
|
||||
| `description` | `string` | Yes | - |
|
||||
| `plugin` | `ChannelPlugin` | Yes | - |
|
||||
| `configSchema` | `OpenClawPluginConfigSchema \| () => OpenClawPluginConfigSchema` | No | Empty object schema |
|
||||
| `setRuntime` | `(runtime: PluginRuntime) => void` | No | - |
|
||||
| `registerCliMetadata` | `(api: OpenClawPluginApi) => void` | No | - |
|
||||
| `registerFull` | `(api: OpenClawPluginApi) => void` | No | - |
|
||||
| Field | Type | Required | Default |
|
||||
| ---------------------- | ---------------------------------------------------------------- | -------- | ------------------- |
|
||||
| `id` | `string` | Yes | - |
|
||||
| `name` | `string` | Yes | - |
|
||||
| `description` | `string` | Yes | - |
|
||||
| `plugin` | `ChannelPlugin` | Yes | - |
|
||||
| `configSchema` | `OpenClawPluginConfigSchema \| () => OpenClawPluginConfigSchema` | No | Empty object schema |
|
||||
| `setRuntime` | `(runtime: PluginRuntime) => void` | No | - |
|
||||
| `registerCliMetadata` | `(api: OpenClawPluginApi) => void` | No | - |
|
||||
| `registerFull` | `(api: OpenClawPluginApi) => void` | No | - |
|
||||
| `registerCapabilities` | `(api: OpenClawPluginApi) => void` | No | - |
|
||||
|
||||
Callbacks run per registration mode (full table under
|
||||
[Registration mode](#registration-mode)):
|
||||
@@ -214,10 +219,13 @@ Callbacks run per registration mode (full table under
|
||||
plugin loads.
|
||||
- `registerFull` runs only for `"full"` and `"tool-discovery"`. For
|
||||
`"tool-discovery"` it runs _instead of_ channel registration: OpenClaw
|
||||
skips `registerChannel`/`setRuntime` entirely and calls only
|
||||
`registerFull`, so any provider/tool registration your channel needs for
|
||||
standalone tool discovery or execution must live there, not behind normal
|
||||
channel setup.
|
||||
skips `registerChannel`/`setRuntime` entirely and calls the full-runtime
|
||||
callback followed by the capability callback. Keep tool registration in
|
||||
`registerFull` and capability providers in `registerCapabilities`.
|
||||
- `registerCapabilities` runs for `"discovery"`, `"full"`, and
|
||||
`"tool-discovery"`. Register inert advertised providers here so read-only
|
||||
capability discovery can find them without starting sockets, clients,
|
||||
workers, or services.
|
||||
- Discovery registration is non-activating, not import-free: OpenClaw may
|
||||
evaluate the trusted plugin entry and channel plugin module to build the
|
||||
snapshot. Keep top-level imports side-effect-free and put sockets,
|
||||
@@ -327,14 +335,14 @@ full activation.
|
||||
|
||||
`api.registrationMode` tells your plugin how it was loaded:
|
||||
|
||||
| Mode | When | What to register |
|
||||
| ------------------ | -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
||||
| `"full"` | Normal gateway startup | Everything |
|
||||
| `"discovery"` | Read-only capability discovery | Channel registration plus static CLI descriptors; entry code may load, but skip sockets, workers, clients, and services |
|
||||
| `"tool-discovery"` | Scoped load to list or run specific plugins' tools | Capability/tool registration only; no channel activation |
|
||||
| `"setup-only"` | Disabled/unconfigured channel | Channel registration only |
|
||||
| `"setup-runtime"` | Setup flow with runtime available | Channel registration plus only the lightweight runtime needed before the full entry loads |
|
||||
| `"cli-metadata"` | Root help / CLI metadata capture | CLI descriptors only |
|
||||
| Mode | When | What to register |
|
||||
| ------------------ | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
|
||||
| `"full"` | Normal gateway startup | Everything |
|
||||
| `"discovery"` | Read-only capability discovery | Channel registration, static CLI descriptors, and inert providers; skip sockets, workers, clients, and services |
|
||||
| `"tool-discovery"` | Scoped load to list or run specific plugins' tools | Capability/tool registration only; no channel activation |
|
||||
| `"setup-only"` | Disabled/unconfigured channel | Channel registration only |
|
||||
| `"setup-runtime"` | Setup flow with runtime available | Channel registration plus only the lightweight runtime needed before the full entry loads |
|
||||
| `"cli-metadata"` | Root help / CLI metadata capture | CLI descriptors only |
|
||||
|
||||
`defineChannelPluginEntry` handles this split automatically. If you use
|
||||
`definePluginEntry` directly for a channel, check mode yourself and remember
|
||||
|
||||
@@ -24,6 +24,8 @@ export default defineBundledChannelEntry({
|
||||
registerFull(api) {
|
||||
registerDiscordActivities(api);
|
||||
registerDiscordSubagentHooks(api);
|
||||
},
|
||||
registerCapabilities(api) {
|
||||
api.registerTranscriptSourceProvider(discordVoiceTranscriptsSourceProvider);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -104,6 +104,7 @@ function createBundledChannelEntry(params: {
|
||||
pluginId: string;
|
||||
registerCliMetadata?: (api: OpenClawPluginApi) => void;
|
||||
registerFull?: (api: OpenClawPluginApi) => void;
|
||||
registerCapabilities?: (api: OpenClawPluginApi) => void;
|
||||
}) {
|
||||
return defineBundledChannelEntry({
|
||||
id: params.pluginId,
|
||||
@@ -114,6 +115,7 @@ function createBundledChannelEntry(params: {
|
||||
runtime: { specifier: "./runtime.cjs", exportName: "setRuntime" },
|
||||
registerCliMetadata: params.registerCliMetadata,
|
||||
registerFull: params.registerFull,
|
||||
registerCapabilities: params.registerCapabilities,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -128,6 +130,7 @@ describe("defineBundledChannelEntry", () => {
|
||||
runtimeMarker,
|
||||
});
|
||||
const registerCliMetadata = vi.fn<(api: OpenClawPluginApi) => void>();
|
||||
const registerCapabilities = vi.fn<(api: OpenClawPluginApi) => void>();
|
||||
const registerFull = vi.fn<(api: OpenClawPluginApi) => void>((api) => {
|
||||
api.registerTool(
|
||||
{
|
||||
@@ -145,6 +148,7 @@ describe("defineBundledChannelEntry", () => {
|
||||
pluginId,
|
||||
registerCliMetadata,
|
||||
registerFull,
|
||||
registerCapabilities,
|
||||
});
|
||||
|
||||
const api = createApi("tool-discovery");
|
||||
@@ -153,6 +157,7 @@ describe("defineBundledChannelEntry", () => {
|
||||
expect(api.registerChannel).not.toHaveBeenCalled();
|
||||
expect(registerCliMetadata).not.toHaveBeenCalled();
|
||||
expect(registerFull).toHaveBeenCalledWith(api);
|
||||
expect(registerCapabilities).toHaveBeenCalledExactlyOnceWith(api);
|
||||
expect(api.registerTool).toHaveBeenCalledTimes(1);
|
||||
expect(fs.existsSync(runtimeMarker)).toBe(false);
|
||||
});
|
||||
@@ -168,11 +173,13 @@ describe("defineBundledChannelEntry", () => {
|
||||
});
|
||||
const registerCliMetadata = vi.fn<(api: OpenClawPluginApi) => void>();
|
||||
const registerFull = vi.fn<(api: OpenClawPluginApi) => void>();
|
||||
const registerCapabilities = vi.fn<(api: OpenClawPluginApi) => void>();
|
||||
const entry = createBundledChannelEntry({
|
||||
importerPath,
|
||||
pluginId,
|
||||
registerCliMetadata,
|
||||
registerFull,
|
||||
registerCapabilities,
|
||||
});
|
||||
|
||||
const api = createApi("discovery");
|
||||
@@ -181,6 +188,7 @@ describe("defineBundledChannelEntry", () => {
|
||||
expect(api.registerChannel).toHaveBeenCalledTimes(1);
|
||||
expect(registerCliMetadata).toHaveBeenCalledWith(api);
|
||||
expect(registerFull).not.toHaveBeenCalled();
|
||||
expect(registerCapabilities).toHaveBeenCalledExactlyOnceWith(api);
|
||||
expect(fs.existsSync(runtimeMarker)).toBe(true);
|
||||
});
|
||||
|
||||
@@ -195,17 +203,31 @@ describe("defineBundledChannelEntry", () => {
|
||||
});
|
||||
const registerCliMetadata = vi.fn<(api: OpenClawPluginApi) => void>();
|
||||
const registerFull = vi.fn<(api: OpenClawPluginApi) => void>();
|
||||
const registerCapabilities = vi.fn<(api: OpenClawPluginApi) => void>();
|
||||
const entry = createBundledChannelEntry({
|
||||
importerPath,
|
||||
pluginId,
|
||||
registerCliMetadata,
|
||||
registerFull,
|
||||
registerCapabilities,
|
||||
});
|
||||
|
||||
const cliApi = createApi("cli-metadata");
|
||||
entry.register(cliApi);
|
||||
expect(registerCliMetadata).toHaveBeenCalledWith(cliApi);
|
||||
expect(registerCapabilities).not.toHaveBeenCalled();
|
||||
expect(fs.existsSync(runtimeMarker)).toBe(false);
|
||||
registerCliMetadata.mockClear();
|
||||
|
||||
entry.register(createApi("setup-only"));
|
||||
expect(registerCapabilities).not.toHaveBeenCalled();
|
||||
fs.rmSync(runtimeMarker, { force: true });
|
||||
|
||||
entry.register(createApi("setup-runtime"));
|
||||
expect(fs.existsSync(runtimeMarker)).toBe(true);
|
||||
expect(registerCliMetadata).not.toHaveBeenCalled();
|
||||
expect(registerFull).not.toHaveBeenCalled();
|
||||
expect(registerCapabilities).not.toHaveBeenCalled();
|
||||
|
||||
fs.rmSync(runtimeMarker, { force: true });
|
||||
const fullApi = createApi("full");
|
||||
@@ -213,6 +235,7 @@ describe("defineBundledChannelEntry", () => {
|
||||
expect(fs.existsSync(runtimeMarker)).toBe(true);
|
||||
expect(registerCliMetadata).toHaveBeenCalledWith(fullApi);
|
||||
expect(registerFull).toHaveBeenCalledWith(fullApi);
|
||||
expect(registerCapabilities).toHaveBeenCalledExactlyOnceWith(fullApi);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ type DefineBundledChannelEntryOptions<TPlugin = ChannelPlugin> = {
|
||||
features?: BundledChannelEntryFeatures;
|
||||
registerCliMetadata?: (api: OpenClawPluginApi) => void;
|
||||
registerFull?: (api: OpenClawPluginApi) => void;
|
||||
registerCapabilities?: (api: OpenClawPluginApi) => void;
|
||||
};
|
||||
|
||||
type DefineBundledChannelSetupEntryOptions = {
|
||||
@@ -482,6 +483,7 @@ export function defineBundledChannelEntry<TPlugin = ChannelPlugin>({
|
||||
features,
|
||||
registerCliMetadata,
|
||||
registerFull,
|
||||
registerCapabilities,
|
||||
}: DefineBundledChannelEntryOptions<TPlugin>): BundledChannelEntryContract<TPlugin> {
|
||||
const resolvedConfigSchema: ChannelEntryConfigSchema<TPlugin> =
|
||||
typeof configSchema === "function"
|
||||
@@ -540,6 +542,7 @@ export function defineBundledChannelEntry<TPlugin = ChannelPlugin>({
|
||||
if (api.registrationMode === "tool-discovery") {
|
||||
const profile = createProfiler({ pluginId: id, source: importMetaUrl });
|
||||
profile("bundled-register:registerFull", () => registerFull?.(api));
|
||||
profile("bundled-register:registerCapabilities", () => registerCapabilities?.(api));
|
||||
return;
|
||||
}
|
||||
const profile = createProfiler({ pluginId: id, source: importMetaUrl });
|
||||
@@ -550,6 +553,7 @@ export function defineBundledChannelEntry<TPlugin = ChannelPlugin>({
|
||||
profile("bundled-register:setChannelRuntime", () => setChannelRuntime?.(api.runtime));
|
||||
if (api.registrationMode === "discovery") {
|
||||
profile("bundled-register:registerCliMetadata", () => registerCliMetadata?.(api));
|
||||
profile("bundled-register:registerCapabilities", () => registerCapabilities?.(api));
|
||||
return;
|
||||
}
|
||||
if (api.registrationMode !== "full") {
|
||||
@@ -557,6 +561,7 @@ export function defineBundledChannelEntry<TPlugin = ChannelPlugin>({
|
||||
}
|
||||
profile("bundled-register:registerCliMetadata", () => registerCliMetadata?.(api));
|
||||
profile("bundled-register:registerFull", () => registerFull?.(api));
|
||||
profile("bundled-register:registerCapabilities", () => registerCapabilities?.(api));
|
||||
},
|
||||
loadChannelPlugin,
|
||||
...(loadChannelOutbound ? { loadChannelOutbound } : {}),
|
||||
|
||||
@@ -43,6 +43,7 @@ describe("defineChannelPluginEntry", () => {
|
||||
it("runs tool registrations without channel runtime wiring during tool discovery", () => {
|
||||
const setRuntime = vi.fn<(runtime: PluginRuntime) => void>();
|
||||
const registerCliMetadata = vi.fn<(api: OpenClawPluginApi) => void>();
|
||||
const registerCapabilities = vi.fn<(api: OpenClawPluginApi) => void>();
|
||||
const registerFull = vi.fn<(api: OpenClawPluginApi) => void>((api) => {
|
||||
api.registerTool(
|
||||
{
|
||||
@@ -63,6 +64,7 @@ describe("defineChannelPluginEntry", () => {
|
||||
setRuntime,
|
||||
registerCliMetadata,
|
||||
registerFull,
|
||||
registerCapabilities,
|
||||
});
|
||||
|
||||
const api = createApi("tool-discovery");
|
||||
@@ -72,6 +74,7 @@ describe("defineChannelPluginEntry", () => {
|
||||
expect(setRuntime).not.toHaveBeenCalled();
|
||||
expect(registerCliMetadata).not.toHaveBeenCalled();
|
||||
expect(registerFull).toHaveBeenCalledWith(api);
|
||||
expect(registerCapabilities).toHaveBeenCalledExactlyOnceWith(api);
|
||||
expect(api.registerTool).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -79,6 +82,7 @@ describe("defineChannelPluginEntry", () => {
|
||||
const setRuntime = vi.fn<(runtime: PluginRuntime) => void>();
|
||||
const registerCliMetadata = vi.fn<(api: OpenClawPluginApi) => void>();
|
||||
const registerFull = vi.fn<(api: OpenClawPluginApi) => void>();
|
||||
const registerCapabilities = vi.fn<(api: OpenClawPluginApi) => void>();
|
||||
const entry = defineChannelPluginEntry({
|
||||
id: "runtime-discovery",
|
||||
name: "Runtime Discovery",
|
||||
@@ -87,6 +91,7 @@ describe("defineChannelPluginEntry", () => {
|
||||
setRuntime,
|
||||
registerCliMetadata,
|
||||
registerFull,
|
||||
registerCapabilities,
|
||||
});
|
||||
|
||||
const api = createApi("discovery");
|
||||
@@ -96,12 +101,14 @@ describe("defineChannelPluginEntry", () => {
|
||||
expect(registerCliMetadata).toHaveBeenCalledTimes(1);
|
||||
expect(setRuntime).toHaveBeenCalledWith(api.runtime);
|
||||
expect(registerFull).not.toHaveBeenCalled();
|
||||
expect(registerCapabilities).toHaveBeenCalledExactlyOnceWith(api);
|
||||
});
|
||||
|
||||
it("keeps setup-runtime and full registration wired to runtime helpers", () => {
|
||||
const setRuntime = vi.fn<(runtime: PluginRuntime) => void>();
|
||||
const registerCliMetadata = vi.fn<(api: OpenClawPluginApi) => void>();
|
||||
const registerFull = vi.fn<(api: OpenClawPluginApi) => void>();
|
||||
const registerCapabilities = vi.fn<(api: OpenClawPluginApi) => void>();
|
||||
const entry = defineChannelPluginEntry({
|
||||
id: "runtime-activation",
|
||||
name: "Runtime Activation",
|
||||
@@ -110,13 +117,24 @@ describe("defineChannelPluginEntry", () => {
|
||||
setRuntime,
|
||||
registerCliMetadata,
|
||||
registerFull,
|
||||
registerCapabilities,
|
||||
});
|
||||
|
||||
const cliApi = createApi("cli-metadata");
|
||||
entry.register(cliApi);
|
||||
expect(registerCliMetadata).toHaveBeenCalledWith(cliApi);
|
||||
expect(registerCapabilities).not.toHaveBeenCalled();
|
||||
registerCliMetadata.mockClear();
|
||||
|
||||
entry.register(createApi("setup-only"));
|
||||
expect(registerCapabilities).not.toHaveBeenCalled();
|
||||
|
||||
const setupApi = createApi("setup-runtime");
|
||||
entry.register(setupApi);
|
||||
expect(setRuntime).toHaveBeenCalledWith(setupApi.runtime);
|
||||
expect(registerCliMetadata).not.toHaveBeenCalled();
|
||||
expect(registerFull).not.toHaveBeenCalled();
|
||||
expect(registerCapabilities).not.toHaveBeenCalled();
|
||||
|
||||
setRuntime.mockClear();
|
||||
const fullApi = createApi("full");
|
||||
@@ -124,6 +142,7 @@ describe("defineChannelPluginEntry", () => {
|
||||
expect(setRuntime).toHaveBeenCalledWith(fullApi.runtime);
|
||||
expect(registerCliMetadata).toHaveBeenCalledWith(fullApi);
|
||||
expect(registerFull).toHaveBeenCalledWith(fullApi);
|
||||
expect(registerCapabilities).toHaveBeenCalledExactlyOnceWith(fullApi);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -502,6 +502,7 @@ type DefineChannelPluginEntryOptions<TPlugin = ChannelPlugin> = {
|
||||
setRuntime?: (runtime: PluginRuntime) => void;
|
||||
registerCliMetadata?: (api: OpenClawPluginApi) => void;
|
||||
registerFull?: (api: OpenClawPluginApi) => void;
|
||||
registerCapabilities?: (api: OpenClawPluginApi) => void;
|
||||
};
|
||||
|
||||
type DefinedChannelPluginEntry<TPlugin> = {
|
||||
@@ -575,6 +576,7 @@ export function defineChannelPluginEntry<TPlugin>({
|
||||
setRuntime,
|
||||
registerCliMetadata,
|
||||
registerFull,
|
||||
registerCapabilities,
|
||||
}: DefineChannelPluginEntryOptions<TPlugin>): DefinedChannelPluginEntry<TPlugin> {
|
||||
const resolvedConfigSchema: ChannelEntryConfigSchema<TPlugin> =
|
||||
typeof configSchema === "function"
|
||||
@@ -592,12 +594,14 @@ export function defineChannelPluginEntry<TPlugin>({
|
||||
}
|
||||
if (api.registrationMode === "tool-discovery") {
|
||||
registerFull?.(api);
|
||||
registerCapabilities?.(api);
|
||||
return;
|
||||
}
|
||||
api.registerChannel({ plugin: plugin as ChannelPlugin });
|
||||
setRuntime?.(api.runtime);
|
||||
if (api.registrationMode === "discovery") {
|
||||
registerCliMetadata?.(api);
|
||||
registerCapabilities?.(api);
|
||||
return;
|
||||
}
|
||||
if (api.registrationMode !== "full") {
|
||||
@@ -605,6 +609,7 @@ export function defineChannelPluginEntry<TPlugin>({
|
||||
}
|
||||
registerCliMetadata?.(api);
|
||||
registerFull?.(api);
|
||||
registerCapabilities?.(api);
|
||||
},
|
||||
};
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { afterAll, afterEach, describe, expect, it } from "vitest";
|
||||
import { loadBundledCapabilityRuntimeRegistry } from "./bundled-capability-runtime.js";
|
||||
import type { PluginDiscoveryResult } from "./discovery.js";
|
||||
import { loadOpenClawPlugins } from "./loader.js";
|
||||
import {
|
||||
cleanupPluginLoaderFixturesForTest,
|
||||
resetPluginLoaderTestStateForTest,
|
||||
type TempPlugin,
|
||||
writePlugin,
|
||||
} from "./loader.test-fixtures.js";
|
||||
import { createEmptyPluginRegistry } from "./registry-empty.js";
|
||||
import {
|
||||
captureActivePluginRegistrySnapshot,
|
||||
collectLivePluginRegistries,
|
||||
getActivePluginRegistry,
|
||||
getPluginRegistrationContext,
|
||||
listImportedRuntimePluginIds,
|
||||
setActivePluginRegistry,
|
||||
} from "./runtime.js";
|
||||
|
||||
afterEach(resetPluginLoaderTestStateForTest);
|
||||
afterAll(cleanupPluginLoaderFixturesForTest);
|
||||
|
||||
function discoveryFor(...plugins: TempPlugin[]): PluginDiscoveryResult {
|
||||
return {
|
||||
candidates: plugins.map((plugin) => ({
|
||||
idHint: plugin.id,
|
||||
rootDir: plugin.dir,
|
||||
source: plugin.file,
|
||||
origin: "bundled",
|
||||
})),
|
||||
diagnostics: [],
|
||||
};
|
||||
}
|
||||
|
||||
function writeArtifactPreferencePlugin(id: string): TempPlugin {
|
||||
const plugin = writePlugin({
|
||||
id,
|
||||
filename: "index.ts",
|
||||
body: `export default {
|
||||
id: ${JSON.stringify(id)},
|
||||
register(api) {
|
||||
api.registerProvider({ id: ${JSON.stringify(`${id}-source`)}, label: "Source", auth: [] });
|
||||
},
|
||||
};`,
|
||||
});
|
||||
const distDir = path.join(plugin.dir, "dist");
|
||||
fs.mkdirSync(distDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(distDir, "index.js"),
|
||||
`module.exports = {
|
||||
id: ${JSON.stringify(id)},
|
||||
register(api) {
|
||||
api.registerProvider({ id: ${JSON.stringify(`${id}-built`)}, label: "Built", auth: [] });
|
||||
},
|
||||
};`,
|
||||
);
|
||||
return plugin;
|
||||
}
|
||||
|
||||
function loadCanonicalFixture(plugin: TempPlugin, discovery: PluginDiscoveryResult) {
|
||||
return loadOpenClawPlugins({
|
||||
config: {
|
||||
plugins: {
|
||||
allow: [plugin.id],
|
||||
entries: { [plugin.id]: { enabled: true } },
|
||||
},
|
||||
},
|
||||
discovery,
|
||||
onlyPluginIds: [plugin.id],
|
||||
preferBuiltPluginArtifacts: false,
|
||||
cache: false,
|
||||
activate: false,
|
||||
});
|
||||
}
|
||||
|
||||
describe("loadBundledCapabilityRuntimeRegistry", () => {
|
||||
it("loads only the requested bundled plugin without replacing the active registry", () => {
|
||||
const target = writePlugin({
|
||||
id: "capability-target",
|
||||
body: `module.exports = {
|
||||
id: "capability-target",
|
||||
register(api) {
|
||||
if (api.registrationMode === "discovery") {
|
||||
api.registerProvider({ id: "capability-target", label: "Target", auth: [] });
|
||||
}
|
||||
if (api.registrationMode === "full") {
|
||||
api.registerProvider({ id: "full-only", label: "Full only", auth: [] });
|
||||
}
|
||||
},
|
||||
};`,
|
||||
});
|
||||
const unscoped = writePlugin({
|
||||
id: "capability-unscoped",
|
||||
body: `module.exports = {
|
||||
id: "capability-unscoped",
|
||||
register() { throw new Error("unscoped plugin loaded"); },
|
||||
};`,
|
||||
});
|
||||
const active = createEmptyPluginRegistry();
|
||||
setActivePluginRegistry(active, "existing-registry");
|
||||
const activeSnapshotBefore = captureActivePluginRegistrySnapshot();
|
||||
const liveRegistriesBefore = collectLivePluginRegistries();
|
||||
const registrationContextBefore = getPluginRegistrationContext();
|
||||
|
||||
const registry = loadBundledCapabilityRuntimeRegistry({
|
||||
pluginIds: [target.id],
|
||||
discovery: discoveryFor(target, unscoped),
|
||||
});
|
||||
|
||||
expect(registry.plugins.map((plugin) => plugin.id)).toEqual([target.id]);
|
||||
expect(registry.plugins[0]?.status).toBe("loaded");
|
||||
expect(registry.providers.map((entry) => entry.provider.id)).toEqual([target.id]);
|
||||
expect(getActivePluginRegistry()).toBe(active);
|
||||
expect(captureActivePluginRegistrySnapshot()).toEqual(activeSnapshotBefore);
|
||||
expect(collectLivePluginRegistries()).toEqual(liveRegistriesBefore);
|
||||
expect(getPluginRegistrationContext()).toBe(registrationContextBefore);
|
||||
expect(listImportedRuntimePluginIds()).toContain(target.id);
|
||||
});
|
||||
|
||||
it.each(["source", "built"] as const)(
|
||||
"keeps %s-first artifact preferences isolated across registry snapshots",
|
||||
(firstArtifact) => {
|
||||
const plugin = writeArtifactPreferencePlugin(`capability-${firstArtifact}-first`);
|
||||
const discovery = discoveryFor(plugin);
|
||||
const loadCapability = () =>
|
||||
loadBundledCapabilityRuntimeRegistry({
|
||||
pluginIds: [plugin.id],
|
||||
pluginSdkResolution: "dist",
|
||||
discovery,
|
||||
});
|
||||
const first =
|
||||
firstArtifact === "source" ? loadCanonicalFixture(plugin, discovery) : loadCapability();
|
||||
const second =
|
||||
firstArtifact === "source" ? loadCapability() : loadCanonicalFixture(plugin, discovery);
|
||||
|
||||
expect(first.providers.map((entry) => entry.provider.id)).toEqual([
|
||||
`${plugin.id}-${firstArtifact}`,
|
||||
]);
|
||||
expect(second.providers.map((entry) => entry.provider.id)).toEqual([
|
||||
`${plugin.id}-${firstArtifact === "source" ? "built" : "source"}`,
|
||||
]);
|
||||
expect(first.pluginRuntimeArtifacts).not.toBe(second.pluginRuntimeArtifacts);
|
||||
},
|
||||
);
|
||||
|
||||
it("loads runtime-backed bundled capabilities through the dist SDK shims", () => {
|
||||
const registry = loadBundledCapabilityRuntimeRegistry({
|
||||
pluginIds: ["codex"],
|
||||
pluginSdkResolution: "dist",
|
||||
});
|
||||
|
||||
const plugin = registry.plugins.find((entry) => entry.id === "codex");
|
||||
expect(
|
||||
plugin?.status,
|
||||
JSON.stringify({ plugin, diagnostics: registry.diagnostics }, null, 2),
|
||||
).toBe("loaded");
|
||||
expect(registry.mediaUnderstandingProviders.map((entry) => entry.provider.id)).toEqual([
|
||||
"codex",
|
||||
]);
|
||||
expect(registry.webSearchProviders.map((entry) => entry.provider.id)).toEqual(["codex"]);
|
||||
expect(registry.migrationProviders.map((entry) => entry.provider.id)).toEqual(["codex"]);
|
||||
});
|
||||
|
||||
it("registers Discord voice transcript capabilities without full channel activation", () => {
|
||||
const active = createEmptyPluginRegistry();
|
||||
setActivePluginRegistry(active, "existing-discord-registry");
|
||||
const registry = loadBundledCapabilityRuntimeRegistry({
|
||||
pluginIds: ["discord"],
|
||||
pluginSdkResolution: "dist",
|
||||
});
|
||||
|
||||
const plugin = registry.plugins.find((entry) => entry.id === "discord");
|
||||
expect(
|
||||
plugin?.status,
|
||||
JSON.stringify({ plugin, diagnostics: registry.diagnostics }, null, 2),
|
||||
).toBe("loaded");
|
||||
expect(plugin?.transcriptSourceProviderIds).toEqual(["discord-voice"]);
|
||||
expect(registry.transcriptSourceProviders.map((entry) => entry.provider.id)).toEqual([
|
||||
"discord-voice",
|
||||
]);
|
||||
expect(registry.typedHooks).toEqual([]);
|
||||
expect(getActivePluginRegistry()).toBe(active);
|
||||
});
|
||||
});
|
||||
@@ -1,36 +1,16 @@
|
||||
/** Loads capability providers from bundled plugin public runtime artifacts. */
|
||||
import fs from "node:fs";
|
||||
/** Loads capability providers through the canonical scoped plugin loader. */
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { describeRootFileOpenFailure, openRootFileSync } from "../infra/boundary-file-read.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import {
|
||||
withBundledPluginEnablementCompat,
|
||||
withBundledPluginVitestCompat,
|
||||
} from "./bundled-compat.js";
|
||||
import { resolveBundledPluginRepoEntryPath } from "./bundled-plugin-metadata.js";
|
||||
import { createCapturedPluginRegistration } from "./captured-registration.js";
|
||||
import { resolveOpenClawDevSourceRoot } from "./dev-source-root.js";
|
||||
import { discoverOpenClawPlugins, type PluginDiscoveryResult } from "./discovery.js";
|
||||
import { loadOpenClawPluginsWithInternalOverrides } from "./loader-runtime-load.js";
|
||||
import type { PluginLoadOptions } from "./loader.js";
|
||||
import { loadPluginManifestRegistry } from "./manifest-registry.js";
|
||||
import { unwrapDefaultModuleExport } from "./module-export.js";
|
||||
import {
|
||||
createPluginModuleLoaderCache,
|
||||
getCachedPluginModuleLoader,
|
||||
type PluginModuleLoaderCache,
|
||||
} from "./plugin-module-loader-cache.js";
|
||||
import { createEmptyPluginRegistry } from "./registry-empty.js";
|
||||
import type { PluginRecord, PluginRegistry } from "./registry.js";
|
||||
import {
|
||||
buildPluginLoaderAliasMap,
|
||||
shouldPreferNativeModuleLoad,
|
||||
type PluginSdkResolutionPreference,
|
||||
} from "./sdk-alias.js";
|
||||
import {
|
||||
findUndeclaredPluginToolNames,
|
||||
normalizePluginToolContractNames,
|
||||
} from "./tool-contracts.js";
|
||||
import type { OpenClawPluginDefinition, OpenClawPluginModule } from "./types.js";
|
||||
import type { PluginRuntime } from "./runtime/types.js";
|
||||
import type { PluginSdkResolutionPreference } from "./sdk-alias.js";
|
||||
|
||||
const log = createSubsystemLogger("plugins");
|
||||
|
||||
@@ -65,132 +45,39 @@ function buildVitestCapabilityShimAliasMap(): Record<string, string> {
|
||||
);
|
||||
}
|
||||
|
||||
function applyVitestCapabilityAliasOverrides(params: {
|
||||
aliasMap: Record<string, string>;
|
||||
pluginSdkResolution?: PluginSdkResolutionPreference;
|
||||
env?: PluginLoadOptions["env"];
|
||||
}): Record<string, string> {
|
||||
if (!params.env?.VITEST || params.pluginSdkResolution !== "dist") {
|
||||
return params.aliasMap;
|
||||
}
|
||||
|
||||
return {
|
||||
...params.aliasMap,
|
||||
// Capability contract loads only need a narrow SDK slice. Keep those
|
||||
// helpers on a tiny source graph so Vitest does not pull the dist chunk
|
||||
// bundle that also drags Matrix/WhatsApp code into these tests.
|
||||
...buildVitestCapabilityShimAliasMap(),
|
||||
};
|
||||
}
|
||||
|
||||
function shouldApplyVitestCapabilityAliasOverrides(params: {
|
||||
pluginSdkResolution?: PluginSdkResolutionPreference;
|
||||
env?: PluginLoadOptions["env"];
|
||||
}): boolean {
|
||||
return Boolean(params.env?.VITEST && params.pluginSdkResolution === "dist");
|
||||
}
|
||||
|
||||
function buildBundledCapabilityRuntimeConfig(
|
||||
pluginIds: readonly string[],
|
||||
env?: PluginLoadOptions["env"],
|
||||
): PluginLoadOptions["config"] {
|
||||
): NonNullable<PluginLoadOptions["config"]> {
|
||||
const enablementCompat = withBundledPluginEnablementCompat({
|
||||
config: undefined,
|
||||
pluginIds,
|
||||
});
|
||||
return withBundledPluginVitestCompat({
|
||||
config: enablementCompat,
|
||||
pluginIds,
|
||||
env,
|
||||
});
|
||||
return (
|
||||
withBundledPluginVitestCompat({
|
||||
config: enablementCompat,
|
||||
pluginIds,
|
||||
env,
|
||||
}) ?? {}
|
||||
);
|
||||
}
|
||||
|
||||
function resolvePluginModuleExport(moduleExport: unknown): {
|
||||
definition?: OpenClawPluginDefinition;
|
||||
register?: OpenClawPluginDefinition["register"];
|
||||
} {
|
||||
const resolved = unwrapDefaultModuleExport(moduleExport);
|
||||
if (typeof resolved === "function") {
|
||||
return {
|
||||
register: resolved as OpenClawPluginDefinition["register"],
|
||||
};
|
||||
}
|
||||
if (resolved && typeof resolved === "object") {
|
||||
const definition = resolved as OpenClawPluginDefinition;
|
||||
return {
|
||||
definition,
|
||||
register: definition.register,
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function createCapabilityPluginRecord(params: {
|
||||
id: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
version?: string;
|
||||
source: string;
|
||||
rootDir?: string;
|
||||
workspaceDir?: string;
|
||||
}): PluginRecord {
|
||||
function createCapabilityRegistrationRuntime(
|
||||
config: NonNullable<PluginLoadOptions["config"]>,
|
||||
): Pick<PluginRuntime, "config"> {
|
||||
return {
|
||||
id: params.id,
|
||||
name: params.name ?? params.id,
|
||||
version: params.version,
|
||||
description: params.description,
|
||||
source: params.source,
|
||||
rootDir: params.rootDir,
|
||||
origin: "bundled",
|
||||
workspaceDir: params.workspaceDir,
|
||||
enabled: true,
|
||||
status: "loaded",
|
||||
toolNames: [],
|
||||
hookNames: [],
|
||||
channelIds: [],
|
||||
cliBackendIds: [],
|
||||
providerIds: [],
|
||||
embeddingProviderIds: [],
|
||||
speechProviderIds: [],
|
||||
realtimeTranscriptionProviderIds: [],
|
||||
realtimeVoiceProviderIds: [],
|
||||
mediaUnderstandingProviderIds: [],
|
||||
transcriptSourceProviderIds: [],
|
||||
imageGenerationProviderIds: [],
|
||||
videoGenerationProviderIds: [],
|
||||
musicGenerationProviderIds: [],
|
||||
webFetchProviderIds: [],
|
||||
webSearchProviderIds: [],
|
||||
migrationProviderIds: [],
|
||||
memoryEmbeddingProviderIds: [],
|
||||
agentHarnessIds: [],
|
||||
cliCommands: [],
|
||||
services: [],
|
||||
gatewayDiscoveryServiceIds: [],
|
||||
commands: [],
|
||||
httpRoutes: 0,
|
||||
hookCount: 0,
|
||||
configSchema: true,
|
||||
config: {
|
||||
current: () => config,
|
||||
mutateConfigFile: async () => {
|
||||
throw new Error("Capability discovery cannot mutate plugin configuration.");
|
||||
},
|
||||
replaceConfigFile: async () => {
|
||||
throw new Error("Capability discovery cannot replace plugin configuration.");
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function recordCapabilityLoadError(
|
||||
registry: PluginRegistry,
|
||||
record: PluginRecord,
|
||||
message: string,
|
||||
): void {
|
||||
record.status = "error";
|
||||
record.error = message;
|
||||
registry.plugins.push(record);
|
||||
registry.diagnostics.push({
|
||||
level: "error",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: `failed to load plugin: ${message}`,
|
||||
});
|
||||
log.error(`[plugins] ${record.id} failed to load from ${record.source}: ${message}`);
|
||||
}
|
||||
|
||||
export function loadBundledCapabilityRuntimeRegistry(params: {
|
||||
pluginIds: readonly string[];
|
||||
env?: PluginLoadOptions["env"];
|
||||
@@ -198,348 +85,56 @@ export function loadBundledCapabilityRuntimeRegistry(params: {
|
||||
discovery?: PluginDiscoveryResult;
|
||||
}) {
|
||||
const env = params.env ?? process.env;
|
||||
const devSourceRoot = resolveOpenClawDevSourceRoot(env);
|
||||
const pluginIds = new Set(params.pluginIds);
|
||||
const registry = createEmptyPluginRegistry();
|
||||
const moduleLoaders: PluginModuleLoaderCache = createPluginModuleLoaderCache();
|
||||
|
||||
const getModuleLoader = (modulePath: string) => {
|
||||
const tryNative =
|
||||
shouldPreferNativeModuleLoad(modulePath) &&
|
||||
!(env?.VITEST && params.pluginSdkResolution === "dist");
|
||||
const aliasMap = shouldApplyVitestCapabilityAliasOverrides({
|
||||
pluginSdkResolution: params.pluginSdkResolution,
|
||||
env,
|
||||
})
|
||||
? applyVitestCapabilityAliasOverrides({
|
||||
aliasMap: buildPluginLoaderAliasMap(
|
||||
modulePath,
|
||||
process.argv[1],
|
||||
import.meta.url,
|
||||
params.pluginSdkResolution,
|
||||
devSourceRoot,
|
||||
),
|
||||
pluginSdkResolution: params.pluginSdkResolution,
|
||||
env,
|
||||
})
|
||||
: undefined;
|
||||
return getCachedPluginModuleLoader({
|
||||
cache: moduleLoaders,
|
||||
modulePath,
|
||||
importerUrl: import.meta.url,
|
||||
devSourceRoot,
|
||||
loaderFilename: import.meta.url,
|
||||
...(aliasMap ? { aliasMap } : {}),
|
||||
pluginSdkResolution: params.pluginSdkResolution,
|
||||
tryNative,
|
||||
});
|
||||
};
|
||||
|
||||
const config = buildBundledCapabilityRuntimeConfig(params.pluginIds, env);
|
||||
const discovery = params.discovery ?? discoverOpenClawPlugins({ env });
|
||||
const pluginIds = new Set(params.pluginIds);
|
||||
const manifestRegistry = loadPluginManifestRegistry({
|
||||
config: buildBundledCapabilityRuntimeConfig(params.pluginIds, env),
|
||||
config,
|
||||
env,
|
||||
candidates: discovery.candidates,
|
||||
diagnostics: discovery.diagnostics,
|
||||
});
|
||||
registry.diagnostics.push(...manifestRegistry.diagnostics);
|
||||
const scopedManifestRegistry = {
|
||||
plugins: manifestRegistry.plugins.filter(
|
||||
(plugin) => plugin.origin === "bundled" && pluginIds.has(plugin.id),
|
||||
),
|
||||
diagnostics: manifestRegistry.diagnostics,
|
||||
};
|
||||
const useVitestShims = Boolean(env.VITEST && params.pluginSdkResolution === "dist");
|
||||
|
||||
const manifestByRoot = new Map(
|
||||
manifestRegistry.plugins.map((record) => [record.rootDir, record]),
|
||||
return loadOpenClawPluginsWithInternalOverrides(
|
||||
{
|
||||
config,
|
||||
env,
|
||||
onlyPluginIds: [...params.pluginIds],
|
||||
pluginSdkResolution: params.pluginSdkResolution,
|
||||
cache: false,
|
||||
activate: false,
|
||||
// Channel setup entries cannot register providers; keep their runtime entry in discovery mode.
|
||||
forceFullRuntimeForChannelPlugins: true,
|
||||
preferBuiltPluginArtifacts: useVitestShims,
|
||||
manifestRegistry: scopedManifestRegistry,
|
||||
logger: {
|
||||
info: (message) => log.info(message),
|
||||
warn: (message) => log.warn(message),
|
||||
error: (message) => log.error(message),
|
||||
debug: (message) => log.debug(message),
|
||||
},
|
||||
},
|
||||
{
|
||||
// Discovery needs the current config, but not the full host runtime graph. The registry
|
||||
// still supplies its scoped lazy methods around this narrow base runtime.
|
||||
runtime: createCapabilityRegistrationRuntime(config),
|
||||
moduleLoader: {
|
||||
installNativeSdkResolver: false,
|
||||
loaderFilename: import.meta.url,
|
||||
...(useVitestShims
|
||||
? {
|
||||
aliasOverrides: buildVitestCapabilityShimAliasMap(),
|
||||
tryNative: false,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
);
|
||||
const seenPluginIds = new Set<string>();
|
||||
const repoRoot = process.cwd();
|
||||
|
||||
for (const candidate of discovery.candidates) {
|
||||
const manifest = manifestByRoot.get(candidate.rootDir);
|
||||
if (!manifest || manifest.origin !== "bundled" || !pluginIds.has(manifest.id)) {
|
||||
continue;
|
||||
}
|
||||
if (seenPluginIds.has(manifest.id)) {
|
||||
continue;
|
||||
}
|
||||
seenPluginIds.add(manifest.id);
|
||||
|
||||
const record = createCapabilityPluginRecord({
|
||||
id: manifest.id,
|
||||
name: manifest.name,
|
||||
description: manifest.description,
|
||||
version: manifest.version,
|
||||
source:
|
||||
env?.VITEST && params.pluginSdkResolution === "dist"
|
||||
? (resolveBundledPluginRepoEntryPath({
|
||||
rootDir: repoRoot,
|
||||
pluginId: manifest.id,
|
||||
preferBuilt: true,
|
||||
}) ?? candidate.source)
|
||||
: candidate.source,
|
||||
rootDir: candidate.rootDir,
|
||||
workspaceDir: candidate.workspaceDir,
|
||||
});
|
||||
|
||||
const boundaryLabel = record.source === candidate.source ? "plugin root" : "repo root";
|
||||
const opened = openRootFileSync({
|
||||
absolutePath: record.source,
|
||||
rootPath: record.source === candidate.source ? candidate.rootDir : repoRoot,
|
||||
boundaryLabel,
|
||||
rejectHardlinks: false,
|
||||
skipLexicalRootCheck: true,
|
||||
});
|
||||
if (!opened.ok) {
|
||||
recordCapabilityLoadError(
|
||||
registry,
|
||||
record,
|
||||
describeRootFileOpenFailure({
|
||||
failure: opened,
|
||||
subject: "plugin entry path",
|
||||
boundaryLabel,
|
||||
filePath: record.source,
|
||||
}),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const safeSource = opened.path;
|
||||
fs.closeSync(opened.fd);
|
||||
|
||||
let mod: OpenClawPluginModule | null;
|
||||
try {
|
||||
mod = getModuleLoader(safeSource)(safeSource) as OpenClawPluginModule;
|
||||
} catch (error) {
|
||||
recordCapabilityLoadError(registry, record, String(error));
|
||||
continue;
|
||||
}
|
||||
|
||||
const resolved = resolvePluginModuleExport(mod);
|
||||
const register = resolved.register;
|
||||
if (typeof register !== "function") {
|
||||
record.status = "disabled";
|
||||
record.error = "plugin export missing register(api)";
|
||||
registry.plugins.push(record);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const captured = createCapturedPluginRegistration();
|
||||
register(captured.api);
|
||||
record.cliBackendIds.push(...captured.cliBackends.map((entry) => entry.id));
|
||||
record.providerIds.push(...captured.providers.map((entry) => entry.id));
|
||||
record.embeddingProviderIds.push(...captured.embeddingProviders.map((entry) => entry.id));
|
||||
record.speechProviderIds.push(...captured.speechProviders.map((entry) => entry.id));
|
||||
record.realtimeTranscriptionProviderIds.push(
|
||||
...captured.realtimeTranscriptionProviders.map((entry) => entry.id),
|
||||
);
|
||||
record.realtimeVoiceProviderIds.push(
|
||||
...captured.realtimeVoiceProviders.map((entry) => entry.id),
|
||||
);
|
||||
record.mediaUnderstandingProviderIds.push(
|
||||
...captured.mediaUnderstandingProviders.map((entry) => entry.id),
|
||||
);
|
||||
record.transcriptSourceProviderIds.push(
|
||||
...captured.transcriptSourceProviders.map((entry) => entry.id),
|
||||
);
|
||||
record.imageGenerationProviderIds.push(
|
||||
...captured.imageGenerationProviders.map((entry) => entry.id),
|
||||
);
|
||||
record.videoGenerationProviderIds.push(
|
||||
...captured.videoGenerationProviders.map((entry) => entry.id),
|
||||
);
|
||||
record.musicGenerationProviderIds.push(
|
||||
...captured.musicGenerationProviders.map((entry) => entry.id),
|
||||
);
|
||||
record.webFetchProviderIds.push(...captured.webFetchProviders.map((entry) => entry.id));
|
||||
record.webSearchProviderIds.push(...captured.webSearchProviders.map((entry) => entry.id));
|
||||
record.migrationProviderIds.push(...captured.migrationProviders.map((entry) => entry.id));
|
||||
record.memoryEmbeddingProviderIds.push(
|
||||
...captured.memoryEmbeddingProviders.map((entry) => entry.id),
|
||||
);
|
||||
record.agentHarnessIds.push(...captured.agentHarnesses.map((entry) => entry.id));
|
||||
record.toolNames.push(...captured.tools.map((entry) => entry.name));
|
||||
|
||||
registry.cliBackends.push(
|
||||
...captured.cliBackends.map((backend) => ({
|
||||
pluginId: record.id,
|
||||
pluginName: record.name,
|
||||
backend,
|
||||
source: record.source,
|
||||
rootDir: record.rootDir,
|
||||
})),
|
||||
);
|
||||
registry.textTransforms.push(
|
||||
...captured.textTransforms.map((transforms) => ({
|
||||
pluginId: record.id,
|
||||
pluginName: record.name,
|
||||
transforms,
|
||||
source: record.source,
|
||||
rootDir: record.rootDir,
|
||||
})),
|
||||
);
|
||||
registry.providers.push(
|
||||
...captured.providers.map((provider) => ({
|
||||
pluginId: record.id,
|
||||
pluginName: record.name,
|
||||
provider,
|
||||
source: record.source,
|
||||
rootDir: record.rootDir,
|
||||
})),
|
||||
);
|
||||
registry.embeddingProviders.push(
|
||||
...captured.embeddingProviders.map((provider) => ({
|
||||
pluginId: record.id,
|
||||
pluginName: record.name,
|
||||
provider,
|
||||
source: record.source,
|
||||
rootDir: record.rootDir,
|
||||
})),
|
||||
);
|
||||
registry.speechProviders.push(
|
||||
...captured.speechProviders.map((provider) => ({
|
||||
pluginId: record.id,
|
||||
pluginName: record.name,
|
||||
provider,
|
||||
source: record.source,
|
||||
rootDir: record.rootDir,
|
||||
})),
|
||||
);
|
||||
registry.realtimeTranscriptionProviders.push(
|
||||
...captured.realtimeTranscriptionProviders.map((provider) => ({
|
||||
pluginId: record.id,
|
||||
pluginName: record.name,
|
||||
provider,
|
||||
source: record.source,
|
||||
rootDir: record.rootDir,
|
||||
})),
|
||||
);
|
||||
registry.realtimeVoiceProviders.push(
|
||||
...captured.realtimeVoiceProviders.map((provider) => ({
|
||||
pluginId: record.id,
|
||||
pluginName: record.name,
|
||||
provider,
|
||||
source: record.source,
|
||||
rootDir: record.rootDir,
|
||||
})),
|
||||
);
|
||||
registry.mediaUnderstandingProviders.push(
|
||||
...captured.mediaUnderstandingProviders.map((provider) => ({
|
||||
pluginId: record.id,
|
||||
pluginName: record.name,
|
||||
provider,
|
||||
source: record.source,
|
||||
rootDir: record.rootDir,
|
||||
})),
|
||||
);
|
||||
registry.transcriptSourceProviders.push(
|
||||
...captured.transcriptSourceProviders.map((provider) => ({
|
||||
pluginId: record.id,
|
||||
pluginName: record.name,
|
||||
provider,
|
||||
source: record.source,
|
||||
rootDir: record.rootDir,
|
||||
})),
|
||||
);
|
||||
registry.imageGenerationProviders.push(
|
||||
...captured.imageGenerationProviders.map((provider) => ({
|
||||
pluginId: record.id,
|
||||
pluginName: record.name,
|
||||
provider,
|
||||
source: record.source,
|
||||
rootDir: record.rootDir,
|
||||
})),
|
||||
);
|
||||
registry.videoGenerationProviders.push(
|
||||
...captured.videoGenerationProviders.map((provider) => ({
|
||||
pluginId: record.id,
|
||||
pluginName: record.name,
|
||||
provider,
|
||||
source: record.source,
|
||||
rootDir: record.rootDir,
|
||||
})),
|
||||
);
|
||||
registry.musicGenerationProviders.push(
|
||||
...captured.musicGenerationProviders.map((provider) => ({
|
||||
pluginId: record.id,
|
||||
pluginName: record.name,
|
||||
provider,
|
||||
source: record.source,
|
||||
rootDir: record.rootDir,
|
||||
})),
|
||||
);
|
||||
registry.webFetchProviders.push(
|
||||
...captured.webFetchProviders.map((provider) => ({
|
||||
pluginId: record.id,
|
||||
pluginName: record.name,
|
||||
provider,
|
||||
source: record.source,
|
||||
rootDir: record.rootDir,
|
||||
})),
|
||||
);
|
||||
registry.webSearchProviders.push(
|
||||
...captured.webSearchProviders.map((provider) => ({
|
||||
pluginId: record.id,
|
||||
pluginName: record.name,
|
||||
provider,
|
||||
source: record.source,
|
||||
rootDir: record.rootDir,
|
||||
})),
|
||||
);
|
||||
registry.migrationProviders.push(
|
||||
...captured.migrationProviders.map((provider) => ({
|
||||
pluginId: record.id,
|
||||
pluginName: record.name,
|
||||
provider,
|
||||
source: record.source,
|
||||
rootDir: record.rootDir,
|
||||
})),
|
||||
);
|
||||
registry.memoryEmbeddingProviders.push(
|
||||
...captured.memoryEmbeddingProviders.map((provider) => ({
|
||||
pluginId: record.id,
|
||||
pluginName: record.name,
|
||||
provider,
|
||||
source: record.source,
|
||||
rootDir: record.rootDir,
|
||||
})),
|
||||
);
|
||||
registry.agentHarnesses.push(
|
||||
...captured.agentHarnesses.map((harness) => ({
|
||||
pluginId: record.id,
|
||||
pluginName: record.name,
|
||||
harness,
|
||||
source: record.source,
|
||||
rootDir: record.rootDir,
|
||||
})),
|
||||
);
|
||||
const declaredToolNames = normalizePluginToolContractNames(record.contracts);
|
||||
for (const tool of captured.tools) {
|
||||
const undeclared = findUndeclaredPluginToolNames({
|
||||
declaredNames: declaredToolNames,
|
||||
toolNames: [tool.name],
|
||||
});
|
||||
if (undeclared.length > 0) {
|
||||
registry.diagnostics.push({
|
||||
level: "error",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: `plugin must declare contracts.tools for: ${undeclared.join(", ")}`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
registry.tools.push({
|
||||
pluginId: record.id,
|
||||
pluginName: record.name,
|
||||
factory: () => tool,
|
||||
names: [tool.name],
|
||||
declaredNames: declaredToolNames,
|
||||
optional: false,
|
||||
source: record.source,
|
||||
rootDir: record.rootDir,
|
||||
});
|
||||
}
|
||||
registry.plugins.push(record);
|
||||
} catch (error) {
|
||||
recordCapabilityLoadError(registry, record, String(error));
|
||||
}
|
||||
}
|
||||
|
||||
return registry;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import { collectBundledChannelConfigs } from "./bundled-channel-config-metadata.
|
||||
import {
|
||||
listBundledPluginMetadata,
|
||||
resolveBundledPluginGeneratedPath,
|
||||
resolveBundledPluginRepoEntryPath,
|
||||
} from "./bundled-plugin-metadata.js";
|
||||
|
||||
type BundledPluginMetadata = ReturnType<typeof listBundledPluginMetadata>[number];
|
||||
@@ -854,75 +853,6 @@ describe("bundled plugin metadata", () => {
|
||||
).toBe(path.join(pluginRoot, "index.js"));
|
||||
});
|
||||
|
||||
it("resolves bundled repo entry paths from dist before workspace source", () => {
|
||||
const tempRoot = createGeneratedPluginTempRoot("openclaw-bundled-plugin-repo-entry-");
|
||||
const pluginRoot = path.join(tempRoot, "extensions", "alpha");
|
||||
const distPluginRoot = path.join(tempRoot, "dist", "extensions", "alpha");
|
||||
|
||||
writeJson(path.join(pluginRoot, "package.json"), {
|
||||
name: "@openclaw/alpha",
|
||||
version: "0.0.1",
|
||||
openclaw: {
|
||||
extensions: ["./index.ts"],
|
||||
},
|
||||
});
|
||||
writeJson(path.join(pluginRoot, "openclaw.plugin.json"), {
|
||||
id: "alpha",
|
||||
configSchema: { type: "object" },
|
||||
});
|
||||
fs.writeFileSync(path.join(pluginRoot, "index.ts"), "export const source = true;\n", "utf8");
|
||||
|
||||
expect(
|
||||
resolveBundledPluginRepoEntryPath({
|
||||
rootDir: tempRoot,
|
||||
pluginId: "alpha",
|
||||
preferBuilt: true,
|
||||
}),
|
||||
).toBe(path.join(pluginRoot, "index.ts"));
|
||||
|
||||
fs.mkdirSync(distPluginRoot, { recursive: true });
|
||||
fs.writeFileSync(path.join(distPluginRoot, "index.js"), "export const built = true;\n", "utf8");
|
||||
expect(
|
||||
resolveBundledPluginRepoEntryPath({
|
||||
rootDir: tempRoot,
|
||||
pluginId: "alpha",
|
||||
preferBuilt: true,
|
||||
}),
|
||||
).toBe(path.join(distPluginRoot, "index.js"));
|
||||
});
|
||||
|
||||
it("keeps bundled repo entry path resolution inside the plugin directory", () => {
|
||||
const tempRoot = createGeneratedPluginTempRoot("openclaw-bundled-plugin-repo-contained-");
|
||||
const pluginRoot = path.join(tempRoot, "extensions", "alpha");
|
||||
|
||||
writeJson(path.join(pluginRoot, "package.json"), {
|
||||
name: "@openclaw/alpha",
|
||||
version: "0.0.1",
|
||||
openclaw: {
|
||||
extensions: ["../escape.ts"],
|
||||
},
|
||||
});
|
||||
writeJson(path.join(pluginRoot, "openclaw.plugin.json"), {
|
||||
id: "alpha",
|
||||
configSchema: { type: "object" },
|
||||
});
|
||||
fs.writeFileSync(path.join(tempRoot, "extensions", "escape.ts"), "export {};\n", "utf8");
|
||||
fs.mkdirSync(path.join(tempRoot, "dist", "extensions"), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(tempRoot, "dist", "extensions", "escape.js"),
|
||||
"export {};\n",
|
||||
"utf8",
|
||||
);
|
||||
|
||||
expect(
|
||||
resolveBundledPluginRepoEntryPath({
|
||||
rootDir: tempRoot,
|
||||
pluginId: "alpha",
|
||||
preferBuilt: true,
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("merges runtime channel schema metadata with manifest-owned channel config fields", () => {
|
||||
const tempRoot = createGeneratedPluginTempRoot("openclaw-bundled-plugin-channel-configs-");
|
||||
|
||||
|
||||
@@ -71,13 +71,6 @@ function resolveBundledPluginMetadataScanDir(
|
||||
});
|
||||
}
|
||||
|
||||
function resolveBundledPluginLookupParams(params: { rootDir: string; scanDir?: string }): {
|
||||
rootDir: string;
|
||||
scanDir?: string;
|
||||
} {
|
||||
return params.scanDir ? params : { rootDir: params.rootDir };
|
||||
}
|
||||
|
||||
function collectBundledPluginMetadata(
|
||||
resolvedScanDir: string | undefined,
|
||||
includeChannelConfigs: boolean,
|
||||
@@ -327,46 +320,3 @@ function resolveBundledPluginEntryCandidate(baseDir: string, entryPath: string):
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
/** Resolves the repo entry path for a bundled plugin, preferring source unless requested. */
|
||||
export function resolveBundledPluginRepoEntryPath(params: {
|
||||
rootDir: string;
|
||||
pluginId: string;
|
||||
preferBuilt?: boolean;
|
||||
scanDir?: string;
|
||||
}): string | null {
|
||||
const metadata = findBundledPluginMetadataById(params.pluginId, {
|
||||
...resolveBundledPluginLookupParams({
|
||||
rootDir: params.rootDir,
|
||||
scanDir: params.scanDir,
|
||||
}),
|
||||
includeChannelConfigs: false,
|
||||
includeSyntheticChannelConfigs: false,
|
||||
});
|
||||
if (!metadata) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const entryOrder = params.preferBuilt
|
||||
? [metadata.source.built, metadata.source.source]
|
||||
: [metadata.source.source, metadata.source.built];
|
||||
const baseDirs = listBundledPluginEntryBaseDirs({
|
||||
rootDir: params.rootDir,
|
||||
pluginDirName: metadata.dirName,
|
||||
...(params.scanDir ? { scanDir: params.scanDir } : {}),
|
||||
});
|
||||
|
||||
for (const baseDir of baseDirs) {
|
||||
for (const entryPath of entryOrder) {
|
||||
const candidate = resolveBundledPluginEntryCandidate(baseDir, entryPath);
|
||||
if (!candidate) {
|
||||
continue;
|
||||
}
|
||||
if (fs.existsSync(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -42,21 +42,19 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("plugin contract registry scoped retries", () => {
|
||||
it("retries provider loads after a transient plugin-scoped runtime error", async () => {
|
||||
it("retries when a manifest-declared provider has no runtime entry", async () => {
|
||||
const loadBundledCapabilityRuntimeRegistry = vi
|
||||
.fn()
|
||||
.mockReturnValueOnce(
|
||||
createMockRuntimeRegistry({
|
||||
plugin: {
|
||||
id: "arcee",
|
||||
status: "error",
|
||||
error: "transient arcee load failure",
|
||||
providerIds: [],
|
||||
status: "loaded",
|
||||
providerIds: ["arcee"],
|
||||
webFetchProviderIds: [],
|
||||
webSearchProviderIds: [],
|
||||
migrationProviderIds: [],
|
||||
},
|
||||
diagnostics: [{ pluginId: "arcee", message: "transient arcee load failure" }],
|
||||
}),
|
||||
)
|
||||
.mockReturnValueOnce(
|
||||
|
||||
@@ -136,13 +136,22 @@ function formatBundledCapabilityPluginLoadError(params: {
|
||||
const diagnostics = params.registry.diagnostics
|
||||
.filter((entry) => entry.pluginId === params.pluginId)
|
||||
.map((entry) => entry.message);
|
||||
const providerIds = params.registry.providers
|
||||
.filter((entry) => entry.pluginId === params.pluginId)
|
||||
.map((entry) => entry.provider.id);
|
||||
const webFetchProviderIds = params.registry.webFetchProviders
|
||||
.filter((entry) => entry.pluginId === params.pluginId)
|
||||
.map((entry) => entry.provider.id);
|
||||
const webSearchProviderIds = params.registry.webSearchProviders
|
||||
.filter((entry) => entry.pluginId === params.pluginId)
|
||||
.map((entry) => entry.provider.id);
|
||||
const detailParts = plugin
|
||||
? [
|
||||
`status=${plugin.status}`,
|
||||
...(plugin.error ? [`error=${plugin.error}`] : []),
|
||||
`providerIds=[${plugin.providerIds.join(", ")}]`,
|
||||
`webFetchProviderIds=[${plugin.webFetchProviderIds.join(", ")}]`,
|
||||
`webSearchProviderIds=[${plugin.webSearchProviderIds.join(", ")}]`,
|
||||
`providerIds=[${providerIds.join(", ")}]`,
|
||||
`webFetchProviderIds=[${webFetchProviderIds.join(", ")}]`,
|
||||
`webSearchProviderIds=[${webSearchProviderIds.join(", ")}]`,
|
||||
]
|
||||
: ["plugin record missing"];
|
||||
if (diagnostics.length > 0) {
|
||||
@@ -157,13 +166,11 @@ function loadScopedCapabilityRuntimeRegistryEntries<T>(params: {
|
||||
pluginId: string;
|
||||
capabilityLabel: string;
|
||||
loadEntries: (registry: BundledCapabilityRuntimeRegistry) => T[];
|
||||
loadDeclaredIds: (
|
||||
plugin: BundledCapabilityRuntimeRegistry["plugins"][number],
|
||||
) => readonly string[];
|
||||
}): T[] {
|
||||
const discovery = discoverOpenClawPlugins({});
|
||||
let lastFailure: Error | undefined;
|
||||
|
||||
// Manifest IDs exist before registration; only observed runtime entries prove the load worked.
|
||||
for (let attempt = 0; attempt < 2; attempt += 1) {
|
||||
const registry = loadBundledCapabilityRuntimeRegistry({
|
||||
pluginIds: [params.pluginId],
|
||||
@@ -175,18 +182,11 @@ function loadScopedCapabilityRuntimeRegistryEntries<T>(params: {
|
||||
return entries;
|
||||
}
|
||||
|
||||
const plugin = registry.plugins.find((entry) => entry.id === params.pluginId);
|
||||
lastFailure = formatBundledCapabilityPluginLoadError({
|
||||
pluginId: params.pluginId,
|
||||
capabilityLabel: params.capabilityLabel,
|
||||
registry,
|
||||
});
|
||||
const shouldRetry =
|
||||
attempt === 0 &&
|
||||
(!plugin || plugin.status !== "loaded" || params.loadDeclaredIds(plugin).length === 0);
|
||||
if (!shouldRetry) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
throw (
|
||||
@@ -223,7 +223,6 @@ function loadProviderContractEntriesForPluginId(pluginId: string): ProviderContr
|
||||
pluginId: entry.pluginId,
|
||||
provider: entry.provider,
|
||||
})),
|
||||
loadDeclaredIds: (plugin) => plugin.providerIds,
|
||||
}).map((entry) => ({
|
||||
pluginId: entry.pluginId,
|
||||
provider: entry.provider,
|
||||
@@ -278,7 +277,6 @@ export function resolveWebFetchProviderContractEntriesForPluginId(
|
||||
provider: entry.provider,
|
||||
credentialValue: resolveWebFetchCredentialValue(entry.provider),
|
||||
})),
|
||||
loadDeclaredIds: (plugin) => plugin.webFetchProviderIds,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -307,7 +305,6 @@ export function resolveWebSearchProviderContractEntriesForPluginId(
|
||||
provider: entry.provider,
|
||||
credentialValue: resolveWebSearchCredentialValue(entry.provider),
|
||||
})),
|
||||
loadDeclaredIds: (plugin) => plugin.webSearchProviderIds,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -109,30 +109,41 @@ export function runPluginRegisterSyncInRegistry(
|
||||
export function createPluginModuleLoader(options: {
|
||||
devSourceRoot?: string | null;
|
||||
pluginSdkResolution?: PluginSdkResolutionPreference;
|
||||
aliasOverrides?: Readonly<Record<string, string>>;
|
||||
tryNative?: boolean;
|
||||
loaderFilename?: string;
|
||||
installNativeSdkResolver?: boolean;
|
||||
}) {
|
||||
const moduleLoaders: PluginModuleLoaderCache = createPluginModuleLoaderCache();
|
||||
const createLoaderForModule = (modulePath: string) => {
|
||||
installOpenClawPluginSdkNativeResolver({
|
||||
argv1: process.argv[1],
|
||||
moduleUrl: import.meta.url,
|
||||
pluginModulePath: modulePath,
|
||||
devSourceRoot: options.devSourceRoot,
|
||||
pluginSdkResolution: options.pluginSdkResolution,
|
||||
});
|
||||
if (options.installNativeSdkResolver !== false && options.tryNative !== false) {
|
||||
installOpenClawPluginSdkNativeResolver({
|
||||
argv1: process.argv[1],
|
||||
moduleUrl: import.meta.url,
|
||||
pluginModulePath: modulePath,
|
||||
devSourceRoot: options.devSourceRoot,
|
||||
pluginSdkResolution: options.pluginSdkResolution,
|
||||
});
|
||||
}
|
||||
const defaultAliasMap = buildPluginLoaderAliasMap(
|
||||
modulePath,
|
||||
process.argv[1],
|
||||
import.meta.url,
|
||||
options.pluginSdkResolution,
|
||||
options.devSourceRoot,
|
||||
);
|
||||
const aliasMap = options.aliasOverrides
|
||||
? { ...defaultAliasMap, ...options.aliasOverrides }
|
||||
: defaultAliasMap;
|
||||
return getCachedPluginModuleLoader({
|
||||
cache: moduleLoaders,
|
||||
modulePath,
|
||||
importerUrl: import.meta.url,
|
||||
loaderFilename: modulePath,
|
||||
loaderFilename: options.loaderFilename ?? modulePath,
|
||||
devSourceRoot: options.devSourceRoot,
|
||||
aliasMap: buildPluginLoaderAliasMap(
|
||||
modulePath,
|
||||
process.argv[1],
|
||||
import.meta.url,
|
||||
options.pluginSdkResolution,
|
||||
options.devSourceRoot,
|
||||
),
|
||||
aliasMap,
|
||||
pluginSdkResolution: options.pluginSdkResolution,
|
||||
...(options.tryNative !== undefined ? { tryNative: options.tryNative } : {}),
|
||||
});
|
||||
};
|
||||
return (modulePath: string): unknown =>
|
||||
|
||||
@@ -30,8 +30,33 @@ import { createPluginIdScopeSet, normalizePluginIdScope } from "./plugin-scope.j
|
||||
import { createEmptyPluginRegistry } from "./registry-empty.js";
|
||||
import { createPluginRegistry, type PluginRegistry } from "./registry.js";
|
||||
import { getActivePluginRegistry } from "./runtime.js";
|
||||
import type { PluginRuntime } from "./runtime/types.js";
|
||||
|
||||
type PluginModuleLoaderOverrides = Pick<
|
||||
Parameters<typeof createPluginModuleLoader>[0],
|
||||
"aliasOverrides" | "tryNative" | "loaderFilename" | "installNativeSdkResolver"
|
||||
>;
|
||||
type InternalPluginLoadOverrides = {
|
||||
moduleLoader: PluginModuleLoaderOverrides;
|
||||
runtime: Pick<PluginRuntime, "config">;
|
||||
};
|
||||
|
||||
export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegistry {
|
||||
return loadOpenClawPluginsInternal(options);
|
||||
}
|
||||
|
||||
/** Internal entry for host-owned snapshots that need a narrow registration runtime. */
|
||||
export function loadOpenClawPluginsWithInternalOverrides(
|
||||
options: PluginLoadOptions & { cache: false },
|
||||
overrides: InternalPluginLoadOverrides,
|
||||
): PluginRegistry {
|
||||
return loadOpenClawPluginsInternal(options, overrides);
|
||||
}
|
||||
|
||||
function loadOpenClawPluginsInternal(
|
||||
options: PluginLoadOptions,
|
||||
overrides?: InternalPluginLoadOverrides,
|
||||
): PluginRegistry {
|
||||
const requestedOnlyPluginIds = normalizePluginIdScope(options.onlyPluginIds);
|
||||
const requestedOnlyPluginIdSet = createPluginIdScopeSet(requestedOnlyPluginIds);
|
||||
if (requestedOnlyPluginIdSet && requestedOnlyPluginIdSet.size === 0) {
|
||||
@@ -80,13 +105,17 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
const loadPluginModule = createPluginModuleLoader({
|
||||
devSourceRoot: context.devSourceRoot,
|
||||
pluginSdkResolution: options.pluginSdkResolution,
|
||||
...overrides?.moduleLoader,
|
||||
});
|
||||
const runtime = createLazyPluginRuntime({
|
||||
devSourceRoot: context.devSourceRoot,
|
||||
pluginSdkResolution: options.pluginSdkResolution,
|
||||
runtimeOptions: options.runtimeOptions,
|
||||
loadPluginModule,
|
||||
});
|
||||
const runtime = overrides?.runtime
|
||||
? // The registry wraps this discovery-only base with scoped lazy capabilities.
|
||||
(overrides.runtime as unknown as PluginRuntime)
|
||||
: createLazyPluginRuntime({
|
||||
devSourceRoot: context.devSourceRoot,
|
||||
pluginSdkResolution: options.pluginSdkResolution,
|
||||
runtimeOptions: options.runtimeOptions,
|
||||
loadPluginModule,
|
||||
});
|
||||
registryBuilder = createPluginRegistry({
|
||||
logger,
|
||||
runtime,
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
resolvePluginLoaderModuleConfig,
|
||||
resolvePluginLoaderTryNative,
|
||||
resolvePluginRuntimeModulePathWithDiagnostics,
|
||||
shouldPreferNativeModuleLoad,
|
||||
type PluginSdkResolutionPreference,
|
||||
} from "./sdk-alias.js";
|
||||
import {
|
||||
@@ -1788,9 +1787,9 @@ describe("plugin sdk alias helpers", () => {
|
||||
});
|
||||
|
||||
it("uses transpiled module loads for source TypeScript plugin entries", () => {
|
||||
expect(shouldPreferNativeModuleLoad("/repo/dist/plugins/runtime/index.js")).toBe(true);
|
||||
expect(resolvePluginLoaderTryNative("/repo/dist/plugins/runtime/index.js")).toBe(true);
|
||||
expect(
|
||||
shouldPreferNativeModuleLoad(
|
||||
resolvePluginLoaderTryNative(
|
||||
`/repo/${bundledPluginFile("discord", "src/channel.runtime.ts")}`,
|
||||
),
|
||||
).toBe(false);
|
||||
@@ -1807,9 +1806,9 @@ describe("plugin sdk alias helpers", () => {
|
||||
});
|
||||
|
||||
try {
|
||||
expect(shouldPreferNativeModuleLoad("/repo/dist/plugins/runtime/index.js")).toBe(false);
|
||||
expect(resolvePluginLoaderTryNative("/repo/dist/plugins/runtime/index.js")).toBe(false);
|
||||
expect(
|
||||
shouldPreferNativeModuleLoad(`/repo/${bundledDistPluginFile("browser", "index.js")}`),
|
||||
resolvePluginLoaderTryNative(`/repo/${bundledDistPluginFile("browser", "index.js")}`),
|
||||
).toBe(false);
|
||||
} finally {
|
||||
Object.defineProperty(process, "versions", {
|
||||
@@ -1827,9 +1826,9 @@ describe("plugin sdk alias helpers", () => {
|
||||
});
|
||||
|
||||
try {
|
||||
expect(shouldPreferNativeModuleLoad("/repo/dist/plugins/runtime/index.js")).toBe(true);
|
||||
expect(resolvePluginLoaderTryNative("/repo/dist/plugins/runtime/index.js")).toBe(true);
|
||||
expect(
|
||||
shouldPreferNativeModuleLoad(`/repo/${bundledDistPluginFile("browser", "index.js")}`),
|
||||
resolvePluginLoaderTryNative(`/repo/${bundledDistPluginFile("browser", "index.js")}`),
|
||||
).toBe(true);
|
||||
} finally {
|
||||
Object.defineProperty(process, "platform", {
|
||||
@@ -1867,8 +1866,7 @@ describe("plugin sdk alias helpers", () => {
|
||||
|
||||
it("prefers native module loading for bundled plugin dist .js modules, keeps .ts on aliased path", () => {
|
||||
// Built .js/.mjs/.cjs files under dist/extensions/ should now delegate
|
||||
// to shouldPreferNativeModuleLoad() — which returns true on Node for
|
||||
// compiled artifacts, avoiding the slow jiti transform path.
|
||||
// to native loading on Node for compiled artifacts, avoiding the slow jiti transform path.
|
||||
expect(
|
||||
resolvePluginLoaderTryNative(`/repo/${bundledDistPluginFile("browser", "index.js")}`, {
|
||||
preferBuiltDist: true,
|
||||
|
||||
@@ -1608,7 +1608,7 @@ function isBundledPluginDistModulePath(modulePath: string): boolean {
|
||||
return modulePath.replace(/\\/g, "/").includes("/dist/extensions/");
|
||||
}
|
||||
|
||||
export function shouldPreferNativeModuleLoad(modulePath: string): boolean {
|
||||
function shouldPreferNativeModuleLoad(modulePath: string): boolean {
|
||||
if (!supportsNativeModuleRuntime()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user