mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:40:44 +00:00
fix(cli): avoid directory plugin reinstall prompts
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user