fix(cli): avoid directory plugin reinstall prompts

This commit is contained in:
Peter Steinberger
2026-05-02 06:14:20 +01:00
parent 6fd197c8a1
commit 04b9f5fc98
5 changed files with 140 additions and 7 deletions

View File

@@ -23,6 +23,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- CLI/directory: report unsupported directory operations for installed channel plugins instead of prompting to reinstall the plugin when it lacks a directory adapter. Fixes #75770. Thanks @lawong888.
- Feishu: preserve Feishu/Lark HTTP error bodies for message sends, media sends, and chat member lookups, so HTTP 400 failures include vendor code, message, log id, and troubleshooter details. Fixes #73860. Thanks @desksk.
- Agents/transcripts: avoid reopening large Pi transcript files through the synchronous session manager for maintenance rewrites, persisted tool-result truncation, manual compaction boundary hardening, and queued compaction rotation. Thanks @mariozechner.
- Web search/Exa: accept `plugins.entries.exa.config.webSearch.baseUrl`, normalize it to the Exa `/search` endpoint, and partition cached results by endpoint. Fixes #54928 and supersedes #54939. Thanks @mrpl327 and @lyfuci.

View File

@@ -20,6 +20,7 @@ Directory lookups for channels that support it (contacts/peers, groups, and “m
- `directory` is meant to help you find IDs you can paste into other commands (especially `openclaw message send --target ...`).
- For many channels, results are config-backed (allowlists / configured groups) rather than a live provider directory.
- Installed channel plugins can still omit directory support; in that case the command reports the unsupported directory operation instead of reinstalling the plugin.
- Default output is `id` (and sometimes `name`) separated by a tab; use `--json` for scripting.
## Using results with `message send`

View File

@@ -233,4 +233,36 @@ describe("registerDirectoryCli", () => {
JSON.stringify([{ id: "channel:config", kind: "group" }], null, 2),
);
});
it("reports unsupported directory capability instead of continuing setup for installed plugins", async () => {
mocks.resolveInstallableChannelPlugin.mockResolvedValue({
cfg: { channels: { "openclaw-weixin": {} } },
channelId: "openclaw-weixin",
plugin: {
id: "openclaw-weixin",
},
configChanged: false,
pluginInstalled: false,
});
const program = new Command().name("openclaw");
registerDirectoryCli(program);
await expect(
program.parseAsync(["directory", "peers", "list", "--channel", "openclaw-weixin"], {
from: "user",
}),
).rejects.toThrow("exit:1");
expect(mocks.resolveInstallableChannelPlugin).toHaveBeenCalledWith(
expect.objectContaining({
rawChannel: "openclaw-weixin",
allowInstall: true,
}),
);
expect(mocks.replaceConfigFile).not.toHaveBeenCalled();
expect(runtimeState.defaultRuntime.error).toHaveBeenCalledWith(
expect.stringContaining("Channel openclaw-weixin does not support directory peers"),
);
});
});

View File

@@ -156,4 +156,93 @@ describe("resolveInstallableChannelPlugin", () => {
}),
);
});
it("returns an existing plugin that lacks the requested capability without reinstalling", async () => {
const catalogEntry = createCatalogEntry({
id: "openclaw-weixin",
pluginId: "@tencent-weixin/openclaw-weixin",
origin: "bundled",
});
const installedPlugin = createPlugin("openclaw-weixin");
mocks.listChannelPluginCatalogEntries.mockReturnValue([catalogEntry]);
mocks.getChannelPlugin.mockReturnValue(installedPlugin);
const result = await resolveInstallableChannelPlugin({
cfg: { plugins: { enabled: true } },
runtime: {} as never,
rawChannel: "openclaw-weixin",
allowInstall: true,
supports: (plugin) => Boolean(plugin.directory),
});
expect(result.plugin).toBe(installedPlugin);
expect(result.pluginInstalled).toBe(false);
expect(result.supportsRequestedCapability).toBe(false);
expect(mocks.ensureChannelSetupPluginInstalled).not.toHaveBeenCalled();
});
it("returns a scoped installed plugin that lacks the requested capability without reinstalling", async () => {
const catalogEntry = createCatalogEntry({
id: "openclaw-weixin",
pluginId: "@tencent-weixin/openclaw-weixin",
origin: "bundled",
});
const scopedPlugin = createPlugin("openclaw-weixin");
mocks.listChannelPluginCatalogEntries.mockReturnValue([catalogEntry]);
mocks.loadChannelSetupPluginRegistrySnapshotForChannel.mockReturnValue({
channels: [{ plugin: scopedPlugin }],
channelSetups: [],
});
const result = await resolveInstallableChannelPlugin({
cfg: { plugins: { enabled: true } },
runtime: {} as never,
rawChannel: "openclaw-weixin",
allowInstall: true,
supports: (plugin) => Boolean(plugin.directory),
});
expect(result.plugin).toBe(scopedPlugin);
expect(result.pluginInstalled).toBe(false);
expect(result.supportsRequestedCapability).toBe(false);
expect(mocks.ensureChannelSetupPluginInstalled).not.toHaveBeenCalled();
});
it("still offers install when only a setup fallback lacks the requested capability", async () => {
const catalogEntry = createCatalogEntry({
id: "demo-directory",
pluginId: "@demo/directory",
origin: "bundled",
});
const setupOnlyPlugin = createPlugin("demo-directory");
mocks.listChannelPluginCatalogEntries.mockReturnValue([catalogEntry]);
mocks.loadChannelSetupPluginRegistrySnapshotForChannel.mockReturnValue({
channels: [],
channelSetups: [{ plugin: setupOnlyPlugin }],
});
mocks.ensureChannelSetupPluginInstalled.mockResolvedValueOnce({
cfg: { plugins: { entries: { "@demo/directory": { enabled: true } } } },
installed: true,
pluginId: "@demo/directory",
status: "installed",
});
const result = await resolveInstallableChannelPlugin({
cfg: { plugins: { enabled: true } },
runtime: {} as never,
rawChannel: "demo-directory",
allowInstall: true,
supports: (plugin) => Boolean(plugin.directory),
});
expect(mocks.ensureChannelSetupPluginInstalled).toHaveBeenCalledWith(
expect.objectContaining({
entry: catalogEntry,
}),
);
expect(result.pluginInstalled).toBe(true);
});
});

View File

@@ -32,6 +32,7 @@ type ResolveInstallableChannelPluginResult = {
catalogEntry?: ChannelPluginCatalogEntry;
configChanged: boolean;
pluginInstalled: boolean;
supportsRequestedCapability?: boolean;
};
function resolveWorkspaceDir(cfg: OpenClawConfig) {
@@ -76,17 +77,21 @@ export function resolveCatalogChannelEntry(raw: string, cfg: OpenClawConfig | nu
function findScopedChannelPlugin(
snapshot: ChannelPluginSnapshot,
channelId: ChannelId,
supports: (plugin: ChannelPlugin) => boolean,
): ChannelPlugin | undefined {
return (
snapshot.channels.find((entry) => entry.plugin.id === channelId)?.plugin ??
snapshot.channelSetups.find((entry) => entry.plugin.id === channelId)?.plugin
);
const runtimePlugin = snapshot.channels.find((entry) => entry.plugin.id === channelId)?.plugin;
if (runtimePlugin) {
return runtimePlugin;
}
const setupPlugin = snapshot.channelSetups.find((entry) => entry.plugin.id === channelId)?.plugin;
return setupPlugin && supports(setupPlugin) ? setupPlugin : undefined;
}
function loadScopedChannelPlugin(params: {
cfg: OpenClawConfig;
runtime: RuntimeEnv;
channelId: ChannelId;
supports: (plugin: ChannelPlugin) => boolean;
pluginId?: string;
workspaceDir?: string;
}): ChannelPlugin | undefined {
@@ -97,7 +102,7 @@ function loadScopedChannelPlugin(params: {
...(params.pluginId ? { pluginId: params.pluginId } : {}),
workspaceDir: params.workspaceDir,
});
return findScopedChannelPlugin(snapshot, params.channelId);
return findScopedChannelPlugin(snapshot, params.channelId, params.supports);
}
export async function resolveInstallableChannelPlugin(params: {
@@ -136,7 +141,7 @@ export async function resolveInstallableChannelPlugin(params: {
}
const existing = getChannelPlugin(channelId);
if (existing && supports(existing)) {
if (existing) {
return {
cfg: nextCfg,
channelId,
@@ -144,6 +149,7 @@ export async function resolveInstallableChannelPlugin(params: {
catalogEntry,
configChanged: false,
pluginInstalled: false,
supportsRequestedCapability: supports(existing),
};
}
@@ -153,10 +159,11 @@ export async function resolveInstallableChannelPlugin(params: {
cfg: nextCfg,
runtime: params.runtime,
channelId,
supports,
pluginId: resolvedPluginId,
workspaceDir,
});
if (scoped && supports(scoped)) {
if (scoped) {
return {
cfg: nextCfg,
channelId,
@@ -164,6 +171,7 @@ export async function resolveInstallableChannelPlugin(params: {
catalogEntry,
configChanged: false,
pluginInstalled: false,
supportsRequestedCapability: supports(scoped),
};
}
@@ -182,6 +190,7 @@ export async function resolveInstallableChannelPlugin(params: {
cfg: nextCfg,
runtime: params.runtime,
channelId,
supports,
pluginId: installedPluginId,
workspaceDir: resolveWorkspaceDir(nextCfg),
})
@@ -196,6 +205,7 @@ export async function resolveInstallableChannelPlugin(params: {
: catalogEntry,
configChanged: nextCfg !== params.cfg,
pluginInstalled: installResult.installed,
supportsRequestedCapability: installedPlugin ? supports(installedPlugin) : undefined,
};
}
}