mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:20:43 +00:00
fix(cli): resolve message channel plugin scopes
This commit is contained in:
@@ -17,7 +17,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Channels/sessions: skip last-route writes when inbound session recording explicitly disables creation, so plugin-owned guarded inbound paths cannot create route-only phantom sessions. Carries forward #73009. Thanks @jzakirov.
|
||||
- Channels/Telegram: skip the optional webhook-info API call during polling-mode status checks and startup bot-label probes so long-polling setups avoid an unnecessary Telegram round trip. Carries forward #72990. Thanks @danielgruneberg.
|
||||
- CLI/message: load only the selected channel plugin for targeted `openclaw message` actions, and fall back to configured channel plugins when the channel must be inferred, so scripted sends avoid full bundled plugin registry scans. Fixes #73006. Thanks @jasonftl.
|
||||
- CLI/message: resolve targeted `openclaw message` channels to their owning plugin before loading the registry, and fall back to configured channel plugins when the channel must be inferred, so scripted sends avoid full bundled plugin registry scans without assuming channel ids match plugin ids. Fixes #73006. Thanks @jasonftl.
|
||||
- CLI/models: keep route-first `models status --json` stdout reserved for the JSON payload by routing auth-profile and startup diagnostics to stderr. Fixes #72962. Thanks @vishutdhar.
|
||||
- Sessions: ignore future-dated session activity timestamps during reset freshness checks and cap future `updatedAt` values at the merge boundary so clock-skewed messages cannot keep stale sessions alive forever. Fixes #72989. Thanks @martingarramon.
|
||||
- Sessions: apply search, activity filters, and limits before gateway row enrichment so bounded session lists avoid scanning discarded transcripts. Carries forward #72978. Thanks @yeager.
|
||||
|
||||
@@ -22,7 +22,7 @@ Channel selection:
|
||||
- `--channel` required if more than one channel is configured.
|
||||
- If exactly one channel is configured, it becomes the default.
|
||||
- Values: `discord|googlechat|imessage|matrix|mattermost|msteams|signal|slack|telegram|whatsapp` (Mattermost requires plugin)
|
||||
- `openclaw message` loads only the selected channel plugin when `--channel` or a channel-prefixed target is present; otherwise it loads configured channel plugins for default-channel inference.
|
||||
- `openclaw message` resolves the selected channel to its owning plugin when `--channel` or a channel-prefixed target is present; otherwise it loads configured channel plugins for default-channel inference.
|
||||
|
||||
Target formats (`--target`):
|
||||
|
||||
|
||||
@@ -34,6 +34,10 @@ const mocks = vi.hoisted(() => ({
|
||||
getActivePluginRegistry: vi.fn<typeof import("../plugins/runtime.js").getActivePluginRegistry>(),
|
||||
resolveConfiguredChannelPluginIds:
|
||||
vi.fn<typeof import("../plugins/channel-plugin-ids.js").resolveConfiguredChannelPluginIds>(),
|
||||
resolveDiscoverableScopedChannelPluginIds:
|
||||
vi.fn<
|
||||
typeof import("../plugins/channel-plugin-ids.js").resolveDiscoverableScopedChannelPluginIds
|
||||
>(),
|
||||
resolveChannelPluginIds:
|
||||
vi.fn<typeof import("../plugins/channel-plugin-ids.js").resolveChannelPluginIds>(),
|
||||
resolvePluginRuntimeLoadContext:
|
||||
@@ -59,6 +63,9 @@ vi.mock("../plugins/channel-plugin-ids.js", () => ({
|
||||
resolveConfiguredChannelPluginIds: (
|
||||
...args: Parameters<typeof mocks.resolveConfiguredChannelPluginIds>
|
||||
) => mocks.resolveConfiguredChannelPluginIds(...args),
|
||||
resolveDiscoverableScopedChannelPluginIds: (
|
||||
...args: Parameters<typeof mocks.resolveDiscoverableScopedChannelPluginIds>
|
||||
) => mocks.resolveDiscoverableScopedChannelPluginIds(...args),
|
||||
resolveChannelPluginIds: (...args: Parameters<typeof mocks.resolveChannelPluginIds>) =>
|
||||
mocks.resolveChannelPluginIds(...args),
|
||||
}));
|
||||
@@ -119,12 +126,14 @@ describe("ensurePluginRegistryLoaded", () => {
|
||||
mocks.resolveRuntimePluginRegistry.mockReset();
|
||||
mocks.getActivePluginRegistry.mockReset();
|
||||
mocks.resolveConfiguredChannelPluginIds.mockReset();
|
||||
mocks.resolveDiscoverableScopedChannelPluginIds.mockReset();
|
||||
mocks.resolveChannelPluginIds.mockReset();
|
||||
mocks.resolvePluginRuntimeLoadContext.mockReset();
|
||||
resetPluginRegistryLoadedForTests();
|
||||
|
||||
mocks.getActivePluginRegistry.mockReturnValue(createEmptyPluginRegistry());
|
||||
mocks.resolveRuntimePluginRegistry.mockReturnValue(undefined);
|
||||
mocks.resolveDiscoverableScopedChannelPluginIds.mockReturnValue([]);
|
||||
mocks.resolvePluginRuntimeLoadContext.mockImplementation((options) => {
|
||||
const rawConfig = (options?.config ?? {}) as Record<string, unknown>;
|
||||
return {
|
||||
|
||||
@@ -98,7 +98,7 @@ describe("runMessageAction", () => {
|
||||
|
||||
expect(ensurePluginRegistryLoaded).toHaveBeenCalledWith({
|
||||
scope: "configured-channels",
|
||||
onlyPluginIds: ["discord"],
|
||||
onlyChannelIds: ["discord"],
|
||||
});
|
||||
expect(exitMock).toHaveBeenCalledOnce();
|
||||
expect(exitMock).toHaveBeenCalledWith(0);
|
||||
@@ -117,7 +117,7 @@ describe("runMessageAction", () => {
|
||||
|
||||
expect(ensurePluginRegistryLoaded).toHaveBeenCalledWith({
|
||||
scope: "configured-channels",
|
||||
onlyPluginIds: ["telegram"],
|
||||
onlyChannelIds: ["telegram"],
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -34,14 +34,14 @@ async function runPluginStopHooks(): Promise<void> {
|
||||
|
||||
function resolveMessagePluginLoadOptions(
|
||||
opts: Record<string, unknown>,
|
||||
): { scope: PluginRegistryScope; onlyPluginIds?: string[] } | undefined {
|
||||
): { scope: PluginRegistryScope; onlyChannelIds?: string[] } | undefined {
|
||||
const scopedChannel = resolveMessageSecretScope({
|
||||
channel: opts.channel,
|
||||
target: opts.target,
|
||||
targets: opts.targets,
|
||||
}).channel;
|
||||
if (scopedChannel) {
|
||||
return { scope: "configured-channels", onlyPluginIds: [scopedChannel] };
|
||||
return { scope: "configured-channels", onlyChannelIds: [scopedChannel] };
|
||||
}
|
||||
return { scope: "configured-channels" };
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ const mocks = vi.hoisted(() => ({
|
||||
getActivePluginRegistry: vi.fn<typeof import("../runtime.js").getActivePluginRegistry>(),
|
||||
resolveConfiguredChannelPluginIds:
|
||||
vi.fn<typeof import("../channel-plugin-ids.js").resolveConfiguredChannelPluginIds>(),
|
||||
resolveDiscoverableScopedChannelPluginIds:
|
||||
vi.fn<typeof import("../channel-plugin-ids.js").resolveDiscoverableScopedChannelPluginIds>(),
|
||||
resolveChannelPluginIds:
|
||||
vi.fn<typeof import("../channel-plugin-ids.js").resolveChannelPluginIds>(),
|
||||
applyPluginAutoEnable:
|
||||
@@ -39,6 +41,9 @@ vi.mock("../channel-plugin-ids.js", () => ({
|
||||
resolveConfiguredChannelPluginIds: (
|
||||
...args: Parameters<typeof mocks.resolveConfiguredChannelPluginIds>
|
||||
) => mocks.resolveConfiguredChannelPluginIds(...args),
|
||||
resolveDiscoverableScopedChannelPluginIds: (
|
||||
...args: Parameters<typeof mocks.resolveDiscoverableScopedChannelPluginIds>
|
||||
) => mocks.resolveDiscoverableScopedChannelPluginIds(...args),
|
||||
resolveChannelPluginIds: (...args: Parameters<typeof mocks.resolveChannelPluginIds>) =>
|
||||
mocks.resolveChannelPluginIds(...args),
|
||||
}));
|
||||
@@ -67,6 +72,7 @@ describe("ensurePluginRegistryLoaded", () => {
|
||||
mocks.resolveRuntimePluginRegistry.mockReset();
|
||||
mocks.getActivePluginRegistry.mockReset();
|
||||
mocks.resolveConfiguredChannelPluginIds.mockReset();
|
||||
mocks.resolveDiscoverableScopedChannelPluginIds.mockReset();
|
||||
mocks.resolveChannelPluginIds.mockReset();
|
||||
mocks.applyPluginAutoEnable.mockReset();
|
||||
mocks.resolveAgentWorkspaceDir.mockClear();
|
||||
@@ -95,6 +101,7 @@ describe("ensurePluginRegistryLoaded", () => {
|
||||
demo: ["demo configured"],
|
||||
},
|
||||
}));
|
||||
mocks.resolveDiscoverableScopedChannelPluginIds.mockReturnValue([]);
|
||||
});
|
||||
|
||||
it("uses the shared runtime load context for configured-channel loads", () => {
|
||||
@@ -215,6 +222,54 @@ describe("ensurePluginRegistryLoaded", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("maps explicit channel scopes to owner plugin ids before loading", () => {
|
||||
const rawConfig = { channels: { "external-chat": { token: "configured" } } };
|
||||
mocks.resolveDiscoverableScopedChannelPluginIds.mockReturnValue(["external-chat-plugin"]);
|
||||
|
||||
ensurePluginRegistryLoaded({
|
||||
scope: "configured-channels",
|
||||
config: rawConfig as never,
|
||||
onlyChannelIds: ["external-chat"],
|
||||
});
|
||||
|
||||
expect(mocks.resolveDiscoverableScopedChannelPluginIds).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
config: expect.objectContaining({
|
||||
...rawConfig,
|
||||
plugins: expect.objectContaining({
|
||||
entries: expect.objectContaining({
|
||||
demo: { enabled: true },
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
activationSourceConfig: rawConfig,
|
||||
channelIds: ["external-chat"],
|
||||
workspaceDir: "/resolved-workspace",
|
||||
}),
|
||||
);
|
||||
expect(mocks.loadOpenClawPlugins).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
config: expect.objectContaining({
|
||||
plugins: expect.objectContaining({
|
||||
allow: ["external-chat-plugin"],
|
||||
entries: expect.objectContaining({
|
||||
"external-chat-plugin": { enabled: true },
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
activationSourceConfig: expect.objectContaining({
|
||||
plugins: expect.objectContaining({
|
||||
allow: ["external-chat-plugin"],
|
||||
entries: expect.objectContaining({
|
||||
"external-chat-plugin": { enabled: true },
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
onlyPluginIds: ["external-chat-plugin"],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("forwards explicit empty scopes without widening to channel resolution", () => {
|
||||
ensurePluginRegistryLoaded({
|
||||
scope: "configured-channels",
|
||||
|
||||
@@ -3,6 +3,7 @@ import { withActivatedPluginIds } from "../activation-context.js";
|
||||
import {
|
||||
resolveChannelPluginIds,
|
||||
resolveConfiguredChannelPluginIds,
|
||||
resolveDiscoverableScopedChannelPluginIds,
|
||||
} from "../channel-plugin-ids.js";
|
||||
import { loadOpenClawPlugins, resolveRuntimePluginRegistry } from "../loader.js";
|
||||
import {
|
||||
@@ -90,11 +91,30 @@ export function ensurePluginRegistryLoaded(options?: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
workspaceDir?: string;
|
||||
onlyPluginIds?: string[];
|
||||
onlyChannelIds?: string[];
|
||||
}): void {
|
||||
const scope = options?.scope ?? "all";
|
||||
const requestedPluginIds = normalizePluginIdScope(options?.onlyPluginIds);
|
||||
const scopedLoad = hasExplicitPluginIdScope(requestedPluginIds);
|
||||
const requestedPluginIdsFromOptions = normalizePluginIdScope(options?.onlyPluginIds);
|
||||
const requestedChannelIds = normalizePluginIdScope(options?.onlyChannelIds);
|
||||
const context = resolvePluginRuntimeLoadContext(options);
|
||||
const requestedChannelOwnerPluginIds =
|
||||
requestedChannelIds === undefined
|
||||
? undefined
|
||||
: resolveDiscoverableScopedChannelPluginIds({
|
||||
config: context.config,
|
||||
activationSourceConfig: context.activationSourceConfig,
|
||||
channelIds: requestedChannelIds,
|
||||
workspaceDir: context.workspaceDir,
|
||||
env: context.env,
|
||||
});
|
||||
const requestedPluginIds =
|
||||
requestedChannelOwnerPluginIds === undefined
|
||||
? requestedPluginIdsFromOptions
|
||||
: normalizePluginIdScope([
|
||||
...(requestedPluginIdsFromOptions ?? []),
|
||||
...requestedChannelOwnerPluginIds,
|
||||
]);
|
||||
const scopedLoad = hasExplicitPluginIdScope(requestedPluginIds);
|
||||
const expectedChannelPluginIds = scopedLoad
|
||||
? (requestedPluginIds ?? [])
|
||||
: scope === "configured-channels"
|
||||
@@ -129,14 +149,18 @@ export function ensurePluginRegistryLoaded(options?: {
|
||||
return;
|
||||
}
|
||||
const scopedConfig =
|
||||
!scopedLoad && scope === "configured-channels" && expectedChannelPluginIds.length > 0
|
||||
scope === "configured-channels" &&
|
||||
expectedChannelPluginIds.length > 0 &&
|
||||
(!scopedLoad || requestedChannelOwnerPluginIds !== undefined)
|
||||
? (withActivatedPluginIds({
|
||||
config: context.config,
|
||||
pluginIds: expectedChannelPluginIds,
|
||||
}) ?? context.config)
|
||||
: context.config;
|
||||
const scopedActivationSourceConfig =
|
||||
!scopedLoad && scope === "configured-channels" && expectedChannelPluginIds.length > 0
|
||||
scope === "configured-channels" &&
|
||||
expectedChannelPluginIds.length > 0 &&
|
||||
(!scopedLoad || requestedChannelOwnerPluginIds !== undefined)
|
||||
? (withActivatedPluginIds({
|
||||
config: context.activationSourceConfig,
|
||||
pluginIds: expectedChannelPluginIds,
|
||||
|
||||
Reference in New Issue
Block a user