mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 04:47:03 -06:00
fix(channels): show missing external channel config (#82849)
This commit is contained in:
committed by
GitHub
parent
5040eb5d84
commit
b328f57bc3
@@ -9,6 +9,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/subagents: route group/channel subagent completions through message-tool-only handoffs when required and keep active-requester wake failures from dropping completion delivery. Fixes #82803. Thanks @galiniliev, @yozakura-ava, and @moeedahmed.
|
||||
- Memory-core: scan persisted memory source sessions on startup, comparing on-disk transcripts against the index and marking only missing/newer/resized files dirty for incremental sync. Fixes #82341. (#82341) Thanks @giodl73-repo.
|
||||
- Telegram: keep the top-level default account in the account list when named accounts or bindings are added alongside top-level credentials, preserving default polling while still letting named-only configs resolve to a single account. Fixes #82794. (#82794) Thanks @giodl73-repo.
|
||||
- CLI/channels: show configured official external channels such as Discord in `openclaw channels list` when their plugin package is missing, including the install and doctor repair command instead of reporting no configured channels. Fixes #82813.
|
||||
- WhatsApp: honor forced document delivery for outbound image, GIF, and video media so `forceDocument`/`asDocument` sends preserve original media bytes instead of using compressed media payloads. (#79272) Thanks @itsuzef.
|
||||
- WhatsApp: name outbound document attachments from their MIME type when no filename is provided, so PDF and CSV sends arrive as `file.pdf` and `file.csv` instead of an extensionless `file`. Thanks @mcaxtr.
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ const mocks = vi.hoisted(() => ({
|
||||
isCatalogChannelInstalled: vi.fn<(params: { entry: ChannelPluginCatalogEntry }) => boolean>(
|
||||
() => true,
|
||||
),
|
||||
resolveMissingOfficialExternalChannelPluginRepairHint: vi.fn(),
|
||||
callGateway: vi.fn(),
|
||||
resolveAgentWorkspaceDir: vi.fn(() => "/tmp/workspace"),
|
||||
resolveDefaultAgentId: vi.fn(() => "main"),
|
||||
@@ -54,6 +55,11 @@ vi.mock("./channel-setup/discovery.js", () => ({
|
||||
isCatalogChannelInstalled: mocks.isCatalogChannelInstalled,
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/official-external-plugin-repair-hints.js", () => ({
|
||||
resolveMissingOfficialExternalChannelPluginRepairHint:
|
||||
mocks.resolveMissingOfficialExternalChannelPluginRepairHint,
|
||||
}));
|
||||
|
||||
vi.mock("../agents/agent-scope.js", () => ({
|
||||
resolveAgentWorkspaceDir: mocks.resolveAgentWorkspaceDir,
|
||||
resolveDefaultAgentId: mocks.resolveDefaultAgentId,
|
||||
@@ -120,6 +126,8 @@ describe("channels list", () => {
|
||||
mocks.listTrustedChannelPluginCatalogEntries.mockReturnValue([]);
|
||||
mocks.isCatalogChannelInstalled.mockReset();
|
||||
mocks.isCatalogChannelInstalled.mockReturnValue(true);
|
||||
mocks.resolveMissingOfficialExternalChannelPluginRepairHint.mockReset();
|
||||
mocks.resolveMissingOfficialExternalChannelPluginRepairHint.mockReturnValue(null);
|
||||
mocks.callGateway.mockReset();
|
||||
mocks.callGateway.mockRejectedValue(new Error("gateway unavailable"));
|
||||
});
|
||||
@@ -355,6 +363,92 @@ describe("channels list", () => {
|
||||
expect(output).toContain("--all");
|
||||
});
|
||||
|
||||
it("default output shows configured official external channels when the plugin is missing", async () => {
|
||||
const runtime = createTestRuntime();
|
||||
mocks.listReadOnlyChannelPluginsForConfig.mockReturnValue([]);
|
||||
mocks.listTrustedChannelPluginCatalogEntries.mockReturnValue([
|
||||
createCatalogEntry("discord", "Discord"),
|
||||
]);
|
||||
mocks.isCatalogChannelInstalled.mockReturnValue(false);
|
||||
mocks.resolveMissingOfficialExternalChannelPluginRepairHint.mockReturnValue({
|
||||
pluginId: "discord",
|
||||
channelId: "discord",
|
||||
label: "Discord",
|
||||
installSpec: "@openclaw/discord",
|
||||
installCommand: "openclaw plugins install @openclaw/discord",
|
||||
doctorFixCommand: "openclaw doctor --fix",
|
||||
repairHint:
|
||||
"Install the official external plugin with: openclaw plugins install @openclaw/discord, or run: openclaw doctor --fix.",
|
||||
});
|
||||
mocks.readConfigFileSnapshot.mockResolvedValue({
|
||||
...baseConfigSnapshot,
|
||||
config: {
|
||||
channels: {
|
||||
discord: { enabled: true, token: "secret" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await channelsListCommand({}, runtime);
|
||||
|
||||
expect(mocks.resolveMissingOfficialExternalChannelPluginRepairHint).toHaveBeenCalledWith({
|
||||
config: {
|
||||
channels: {
|
||||
discord: { enabled: true, token: "secret" },
|
||||
},
|
||||
},
|
||||
channelId: "discord",
|
||||
workspaceDir: "/tmp/workspace",
|
||||
});
|
||||
const output = stripAnsi(loggedText(runtime));
|
||||
expect(output).toContain("Discord");
|
||||
expect(output).toContain("not installed");
|
||||
expect(output).toContain("configured");
|
||||
expect(output).toContain("disabled");
|
||||
expect(output).toContain(
|
||||
"run openclaw plugins install @openclaw/discord or openclaw doctor --fix",
|
||||
);
|
||||
expect(output).not.toContain("no configured chat channels");
|
||||
});
|
||||
|
||||
it("JSON output includes configured official external channels when the plugin is missing", async () => {
|
||||
const runtime = createTestRuntime();
|
||||
mocks.listReadOnlyChannelPluginsForConfig.mockReturnValue([]);
|
||||
mocks.listTrustedChannelPluginCatalogEntries.mockReturnValue([
|
||||
createCatalogEntry("discord", "Discord"),
|
||||
]);
|
||||
mocks.isCatalogChannelInstalled.mockReturnValue(false);
|
||||
mocks.resolveMissingOfficialExternalChannelPluginRepairHint.mockReturnValue({
|
||||
pluginId: "discord",
|
||||
channelId: "discord",
|
||||
label: "Discord",
|
||||
installSpec: "@openclaw/discord",
|
||||
installCommand: "openclaw plugins install @openclaw/discord",
|
||||
doctorFixCommand: "openclaw doctor --fix",
|
||||
repairHint:
|
||||
"Install the official external plugin with: openclaw plugins install @openclaw/discord, or run: openclaw doctor --fix.",
|
||||
});
|
||||
mocks.readConfigFileSnapshot.mockResolvedValue({
|
||||
...baseConfigSnapshot,
|
||||
config: {
|
||||
channels: {
|
||||
discord: { enabled: true, token: "secret" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await channelsListCommand({ json: true }, runtime);
|
||||
|
||||
const payload = JSON.parse(loggedText(runtime)) as {
|
||||
chat: Record<string, { accounts: string[]; origin: string; installed: boolean }>;
|
||||
};
|
||||
expect(payload.chat.discord).toEqual({
|
||||
accounts: [],
|
||||
installed: false,
|
||||
origin: "configured",
|
||||
});
|
||||
});
|
||||
|
||||
it("--all surfaces uninstalled catalog channels with installed=false / not configured / not enabled", async () => {
|
||||
const runtime = createTestRuntime();
|
||||
mocks.listReadOnlyChannelPluginsForConfig.mockReturnValue([]);
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
type RuntimeChannelStatusPayload,
|
||||
} from "../../channels/status/read-model.js";
|
||||
import { callGateway } from "../../gateway/call.js";
|
||||
import { resolveMissingOfficialExternalChannelPluginRepairHint } from "../../plugins/official-external-plugin-repair-hints.js";
|
||||
import { defaultRuntime, type RuntimeEnv, writeRuntimeJson } from "../../runtime.js";
|
||||
import { formatDocsLink } from "../../terminal/links.js";
|
||||
import { theme } from "../../terminal/theme.js";
|
||||
@@ -124,14 +125,19 @@ function formatAccountLine(params: {
|
||||
function formatCatalogOnlyLine(params: {
|
||||
entry: ChannelPluginCatalogEntry;
|
||||
installed: boolean;
|
||||
configured: boolean;
|
||||
repairHint?: string;
|
||||
}): string {
|
||||
const { entry, installed } = params;
|
||||
const { entry, installed, configured, repairHint } = params;
|
||||
const channelText = theme.accent(entry.meta.label ?? entry.id);
|
||||
const bits: string[] = [
|
||||
formatInstalled(installed),
|
||||
formatConfigured(false),
|
||||
formatConfigured(configured),
|
||||
formatEnabled(false),
|
||||
];
|
||||
if (repairHint) {
|
||||
bits.push(repairHint);
|
||||
}
|
||||
return `- ${channelText}: ${bits.join(", ")}`;
|
||||
}
|
||||
|
||||
@@ -229,8 +235,8 @@ export async function channelsListCommand(
|
||||
});
|
||||
}
|
||||
|
||||
// --all also surfaces catalog entries that are not already represented
|
||||
// by a plugin row above. Two shapes land here:
|
||||
// Catalog entries that are not already represented by a plugin row above can
|
||||
// still be useful in two shapes:
|
||||
// 1. Catalog plugin package is not yet installed on disk — rendered as
|
||||
// `not installed, not configured, disabled` so the channel still
|
||||
// appears in the listing as installable.
|
||||
@@ -240,9 +246,25 @@ export async function channelsListCommand(
|
||||
// configured channels). These would otherwise silently disappear
|
||||
// from the listing — render them as `installed, not configured,
|
||||
// disabled` so operators can tell the plugin is ready to configure.
|
||||
const catalogOnlyLines: ChannelPluginCatalogEntry[] = showAll
|
||||
? catalogEntries.filter((entry) => !renderedChannelIds.has(entry.id))
|
||||
: [];
|
||||
// Without --all, keep this limited to configured channels whose official
|
||||
// external plugin owner is missing, otherwise `channels list` can claim
|
||||
// there are no configured channels even though openclaw.json has one.
|
||||
const catalogOnlyLines = catalogEntries
|
||||
.filter((entry) => !renderedChannelIds.has(entry.id))
|
||||
.map((entry) => {
|
||||
const hint = resolveMissingOfficialExternalChannelPluginRepairHint({
|
||||
config: cfg,
|
||||
channelId: entry.id,
|
||||
...(workspaceDir ? { workspaceDir } : {}),
|
||||
});
|
||||
return {
|
||||
entry,
|
||||
installed: isInstalled(entry.id),
|
||||
configured: Boolean(hint),
|
||||
repairHint: hint ? `run ${hint.installCommand} or ${hint.doctorFixCommand}` : undefined,
|
||||
};
|
||||
})
|
||||
.filter((line) => showAll || line.configured);
|
||||
|
||||
if (opts.json) {
|
||||
type JsonChannelEntry = {
|
||||
@@ -268,15 +290,12 @@ export async function channelsListCommand(
|
||||
};
|
||||
}
|
||||
}
|
||||
if (showAll) {
|
||||
for (const entry of catalogOnlyLines) {
|
||||
const installed = isInstalled(entry.id);
|
||||
chat[entry.id] = {
|
||||
accounts: [],
|
||||
installed,
|
||||
origin: installed ? "available" : "installable",
|
||||
};
|
||||
}
|
||||
for (const line of catalogOnlyLines) {
|
||||
chat[line.entry.id] = {
|
||||
accounts: [],
|
||||
installed: line.installed,
|
||||
origin: line.configured ? "configured" : line.installed ? "available" : "installable",
|
||||
};
|
||||
}
|
||||
writeRuntimeJson(runtime, { chat });
|
||||
return;
|
||||
@@ -302,11 +321,13 @@ export async function channelsListCommand(
|
||||
}),
|
||||
);
|
||||
}
|
||||
for (const entry of catalogOnlyLines) {
|
||||
for (const line of catalogOnlyLines) {
|
||||
lines.push(
|
||||
formatCatalogOnlyLine({
|
||||
entry,
|
||||
installed: isInstalled(entry.id),
|
||||
entry: line.entry,
|
||||
installed: line.installed,
|
||||
configured: line.configured,
|
||||
...(line.repairHint ? { repairHint: line.repairHint } : {}),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user