fix(channels): surface missing external plugin repairs

## Summary
- Add catalog-backed repair hints for official external channel plugins.
- Show configured Feishu/WhatsApp-style external channels as missing-plugin warning rows in status surfaces.
- Keep installed-but-unconfigured, disabled, allowlist-denied, and untrusted plugins on their real activation/configuration error paths.

Fixes #78702
Fixes #78593
This commit is contained in:
Vincent Koc
2026-05-07 12:49:17 -07:00
committed by GitHub
parent 484a289be3
commit f482e4d335
9 changed files with 452 additions and 5 deletions

View File

@@ -21,6 +21,7 @@ Docs: https://docs.openclaw.ai
- Discord/streaming: default Discord replies to progress draft previews so tool/work activity appears in one edited Discord message unless `channels.discord.streaming.mode` is set to `off`.
- OpenAI: support `openai/chat-latest` as an explicit direct API-key model override for trying the moving ChatGPT Instant API alias without changing the stable default model.
- Plugins/install: add `npm-pack:<path.tgz>` installs so local npm pack artifacts run through the same managed npm-root install, lockfile verification, dependency scan, and install-record path as registry npm plugins.
- Channels/plugins: show configured official external channels as missing-plugin status rows and send errors with exact install/doctor repair commands after raw package-manager upgrades leave Feishu or WhatsApp uninstalled. Fixes #78702 and #78593. Thanks @MarkMa84 and @mkupiainen.
- Codex app-server: disarm the short post-tool completion watchdog after current-turn activity, expose `appServer.turnCompletionIdleTimeoutMs`, and include raw assistant item context in idle-timeout diagnostics so status-only post-tool stalls stop failing as idle. Fixes #77984. Thanks @roseware-dev and @rubencu.
- Plugin skills/Windows: publish plugin-provided skill directories as junctions on Windows so standard users without Developer Mode can register plugin skills without symlink EPERM failures. Fixes #77958. (#77971) Thanks @hclsys and @jarro.
- MS Teams: surface blocked Bot Framework egress by logging JWKS fetch network failures and adding a Bot Connector send hint for transport-level reply failures. Fixes #77674. (#78081) Thanks @Beandon13.

View File

@@ -12,6 +12,7 @@ const mocks = vi.hoisted(() => ({
requireValidConfigSnapshot: vi.fn(),
listChannelPlugins: vi.fn(),
listConfiguredChannelIdsForReadOnlyScope: vi.fn((_params: unknown) => ["discord"]),
missingOfficialExternalChannels: new Set<string>(),
withProgress: vi.fn(async (_opts: unknown, run: () => Promise<unknown>) => await run()),
}));
@@ -36,10 +37,28 @@ vi.mock("../config/config.js", () => ({
}));
vi.mock("../plugins/channel-plugin-ids.js", () => ({
listExplicitConfiguredChannelIdsForConfig: (config: { channels?: Record<string, unknown> }) =>
Object.keys(config.channels ?? {}),
listConfiguredChannelIdsForReadOnlyScope: (params: unknown) =>
mocks.listConfiguredChannelIdsForReadOnlyScope(params),
}));
vi.mock("../plugins/official-external-plugin-repair-hints.js", () => ({
resolveMissingOfficialExternalChannelPluginRepairHint: ({ channelId }: { channelId: string }) =>
mocks.missingOfficialExternalChannels.has(channelId)
? {
pluginId: channelId,
channelId,
label: "Feishu",
installSpec: "@openclaw/feishu",
installCommand: "openclaw plugins install @openclaw/feishu",
doctorFixCommand: "openclaw doctor --fix",
repairHint:
"Install the official external plugin with: openclaw plugins install @openclaw/feishu, or run: openclaw doctor --fix.",
}
: null,
}));
vi.mock("./channels/shared.js", () => ({
requireValidConfigSnapshot: (runtime: unknown) => mocks.requireValidConfigSnapshot(runtime),
formatChannelAccountLabel: ({
@@ -184,6 +203,7 @@ describe("channelsStatusCommand SecretRef fallback flow", () => {
mocks.readConfigFileSnapshot.mockClear();
mocks.requireValidConfigSnapshot.mockReset();
mocks.listChannelPlugins.mockReset();
mocks.missingOfficialExternalChannels.clear();
mocks.listConfiguredChannelIdsForReadOnlyScope.mockClear();
mocks.listConfiguredChannelIdsForReadOnlyScope.mockReturnValue(["discord"]);
mocks.withProgress.mockClear();
@@ -240,6 +260,29 @@ describe("channelsStatusCommand SecretRef fallback flow", () => {
expect(joined).not.toContain("token:config (unavailable)");
});
it("shows missing official external plugin repair hints in config-only output", async () => {
mocks.callGateway.mockRejectedValue(new Error("gateway closed"));
mocks.requireValidConfigSnapshot.mockResolvedValue({
channels: { feishu: { appId: "cli_xxx" } },
});
mocks.resolveCommandConfigWithSecrets.mockResolvedValue({
resolvedConfig: { channels: { feishu: { appId: "cli_xxx" } } },
effectiveConfig: { channels: { feishu: { appId: "cli_xxx" } } },
diagnostics: [],
});
mocks.missingOfficialExternalChannels.add("feishu");
mocks.listChannelPlugins.mockReturnValue([]);
const { runtime, logs } = createCapturingTestRuntime();
await channelsStatusCommand({ probe: false }, runtime as never);
const joined = logs.join("\n");
expect(joined).toContain("Missing official external plugins:");
expect(joined).toContain(
"Feishu: Install the official external plugin with: openclaw plugins install @openclaw/feishu, or run: openclaw doctor --fix.",
);
});
it("keeps JSON fallback structured without rendering config-only text", async () => {
mocks.callGateway.mockRejectedValue(
new Error(

View File

@@ -9,6 +9,11 @@ import {
} from "../../channels/plugins/status.js";
import type { ChannelAccountSnapshot } from "../../channels/plugins/types.public.js";
import type { OpenClawConfig } from "../../config/config.js";
import { listExplicitConfiguredChannelIdsForConfig } from "../../plugins/channel-plugin-ids.js";
import {
type OfficialExternalPluginRepairHint,
resolveMissingOfficialExternalChannelPluginRepairHint,
} from "../../plugins/official-external-plugin-repair-hints.js";
import { formatDocsLink } from "../../terminal/links.js";
import { theme } from "../../terminal/theme.js";
import {
@@ -62,7 +67,9 @@ export async function formatConfigChannelsStatusLines(
activationSourceConfig: sourceConfig,
includeSetupFallbackPlugins: true,
});
const visibleChannelIds = new Set<string>();
for (const plugin of plugins) {
visibleChannelIds.add(plugin.id);
const accountIds = plugin.config.listAccountIds(cfg);
if (!accountIds.length) {
continue;
@@ -93,6 +100,36 @@ export async function formatConfigChannelsStatusLines(
}
}
const missingHints: OfficialExternalPluginRepairHint[] = [];
const missingChannelIds = [
...new Set([
...listExplicitConfiguredChannelIdsForConfig(sourceConfig),
...listExplicitConfiguredChannelIdsForConfig(cfg),
]),
];
for (const channelId of missingChannelIds) {
if (visibleChannelIds.has(channelId)) {
continue;
}
const hint = resolveMissingOfficialExternalChannelPluginRepairHint({
config: cfg,
activationSourceConfig: sourceConfig,
channelId,
});
if (!hint?.channelId || visibleChannelIds.has(hint.channelId)) {
continue;
}
missingHints.push(hint);
visibleChannelIds.add(hint.channelId);
}
if (missingHints.length > 0) {
lines.push("");
lines.push(theme.warn("Missing official external plugins:"));
for (const hint of missingHints) {
lines.push(`- ${hint.label}: ${hint.repairHint}`);
}
}
lines.push("");
lines.push(
`Tip: ${formatDocsLink("/cli#status", "status --deep")} adds gateway health probes to status output (requires a reachable gateway).`,

View File

@@ -3,6 +3,8 @@ import { buildChannelsTable } from "./channels.js";
const mocks = vi.hoisted(() => ({
resolveInspectedChannelAccount: vi.fn(),
listReadOnlyChannelPluginsForConfig: vi.fn(),
missingOfficialExternalChannels: new Set<string>(),
}));
const discordPlugin = {
@@ -18,12 +20,34 @@ vi.mock("../../channels/account-inspection.js", () => ({
}));
vi.mock("../../channels/plugins/read-only.js", () => ({
listReadOnlyChannelPluginsForConfig: () => [discordPlugin],
resolveReadOnlyChannelPluginsForConfig: () => ({
plugins: mocks.listReadOnlyChannelPluginsForConfig(),
configuredChannelIds: [],
missingConfiguredChannelIds: [],
}),
}));
vi.mock("../../plugins/official-external-plugin-repair-hints.js", () => ({
resolveMissingOfficialExternalChannelPluginRepairHint: ({ channelId }: { channelId: string }) =>
mocks.missingOfficialExternalChannels.has(channelId)
? {
pluginId: channelId,
channelId,
label: "Feishu",
installSpec: "@openclaw/feishu",
installCommand: "openclaw plugins install @openclaw/feishu",
doctorFixCommand: "openclaw doctor --fix",
repairHint:
"Install the official external plugin with: openclaw plugins install @openclaw/feishu, or run: openclaw doctor --fix.",
}
: null,
}));
describe("buildChannelsTable", () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.missingOfficialExternalChannels.clear();
mocks.listReadOnlyChannelPluginsForConfig.mockReturnValue([discordPlugin]);
mocks.resolveInspectedChannelAccount.mockResolvedValue({
account: {
tokenStatus: "configured_unavailable",
@@ -79,4 +103,30 @@ describe("buildChannelsTable", () => {
}),
);
});
it("shows configured official external channels when the plugin is missing", async () => {
mocks.listReadOnlyChannelPluginsForConfig.mockReturnValue([]);
mocks.missingOfficialExternalChannels.add("feishu");
const table = await buildChannelsTable({ channels: { feishu: { appId: "cli_xxx" } } });
expect(table.rows).toContainEqual({
id: "feishu",
label: "Feishu",
enabled: true,
state: "warn",
detail:
"plugin not installed - run openclaw plugins install @openclaw/feishu or openclaw doctor --fix",
});
expect(mocks.resolveInspectedChannelAccount).not.toHaveBeenCalled();
});
it("does not show install repair rows when an external channel owner is policy-blocked", async () => {
mocks.listReadOnlyChannelPluginsForConfig.mockReturnValue([]);
const table = await buildChannelsTable({ channels: { feishu: { appId: "cli_xxx" } } });
expect(table.rows).toEqual([]);
expect(mocks.resolveInspectedChannelAccount).not.toHaveBeenCalled();
});
});

View File

@@ -6,7 +6,7 @@ import {
formatChannelAllowFrom,
} from "../../channels/account-summary.js";
import { resolveChannelDefaultAccountId } from "../../channels/plugins/helpers.js";
import { listReadOnlyChannelPluginsForConfig } from "../../channels/plugins/read-only.js";
import { resolveReadOnlyChannelPluginsForConfig } from "../../channels/plugins/read-only.js";
import { formatChannelStatusState } from "../../channels/plugins/status-state.js";
import type {
ChannelAccountSnapshot,
@@ -14,6 +14,8 @@ import type {
ChannelPlugin,
} from "../../channels/plugins/types.public.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { listExplicitConfiguredChannelIdsForConfig } from "../../plugins/channel-plugin-ids.js";
import { resolveMissingOfficialExternalChannelPluginRepairHint } from "../../plugins/official-external-plugin-repair-hints.js";
import { asRecord } from "../../shared/record-coerce.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import {
@@ -272,10 +274,11 @@ export async function buildChannelsTable(
const sourceConfig = opts?.sourceConfig ?? cfg;
const includeSetupFallbackPlugins = opts?.includeSetupFallbackPlugins ?? true;
for (const plugin of listReadOnlyChannelPluginsForConfig(cfg, {
const readOnlyPlugins = resolveReadOnlyChannelPluginsForConfig(cfg, {
activationSourceConfig: sourceConfig,
includeSetupFallbackPlugins,
})) {
});
for (const plugin of readOnlyPlugins.plugins) {
const accountIds = plugin.config.listAccountIds(cfg);
const defaultAccountId = resolveChannelDefaultAccountId({
plugin,
@@ -481,6 +484,36 @@ export async function buildChannelsTable(
}
}
const visibleChannelIds = new Set(rows.map((row) => row.id));
const missingCandidateChannelIds = [
...new Set([
...readOnlyPlugins.missingConfiguredChannelIds,
...listExplicitConfiguredChannelIdsForConfig(sourceConfig),
...listExplicitConfiguredChannelIdsForConfig(cfg),
]),
].toSorted((left, right) => left.localeCompare(right));
for (const channelId of missingCandidateChannelIds) {
if (visibleChannelIds.has(channelId)) {
continue;
}
const hint = resolveMissingOfficialExternalChannelPluginRepairHint({
config: cfg,
activationSourceConfig: sourceConfig,
channelId,
});
if (!hint || hint.channelId !== channelId) {
continue;
}
rows.push({
id: channelId,
label: hint.label,
enabled: true,
state: "warn",
detail: `plugin not installed - run ${hint.installCommand} or ${hint.doctorFixCommand}`,
});
visibleChannelIds.add(channelId);
}
return {
rows,
details,

View File

@@ -3,9 +3,18 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
listChannelPlugins: vi.fn(),
resolveOutboundChannelPlugin: vi.fn(),
missingOfficialExternalChannels: new Set<string>(),
}));
const deliverableChannelIds = vi.hoisted(() => ["alpha", "beta", "gamma", "delta", "muted"]);
const deliverableChannelIds = vi.hoisted(() => [
"alpha",
"beta",
"gamma",
"delta",
"feishu",
"muted",
"whatsapp",
]);
vi.mock("../../channels/plugins/index.js", () => ({
getLoadedChannelPlugin: vi.fn(),
@@ -23,6 +32,21 @@ vi.mock("./channel-resolution.js", () => ({
resolveOutboundChannelPlugin: mocks.resolveOutboundChannelPlugin,
}));
vi.mock("../../plugins/official-external-plugin-repair-hints.js", () => ({
resolveMissingOfficialExternalChannelPluginRepairHint: ({ channelId }: { channelId: string }) =>
mocks.missingOfficialExternalChannels.has(channelId)
? {
pluginId: channelId,
channelId,
label: channelId === "whatsapp" ? "WhatsApp" : "Feishu",
installSpec: `@openclaw/${channelId}`,
installCommand: `openclaw plugins install @openclaw/${channelId}`,
doctorFixCommand: "openclaw doctor --fix",
repairHint: `Install the official external plugin with: openclaw plugins install @openclaw/${channelId}, or run: openclaw doctor --fix.`,
}
: null,
}));
type ChannelSelectionModule = typeof import("./channel-selection.js");
type RuntimeModule = typeof import("../../runtime.js");
@@ -141,6 +165,11 @@ describe("resolveMessageChannelSelection", () => {
beforeEach(() => {
mocks.listChannelPlugins.mockReset();
mocks.listChannelPlugins.mockReturnValue([]);
mocks.resolveOutboundChannelPlugin.mockReset();
mocks.resolveOutboundChannelPlugin.mockImplementation(({ channel }: { channel: string }) => ({
id: channel,
}));
mocks.missingOfficialExternalChannels.clear();
});
it.each([
@@ -228,10 +257,43 @@ describe("resolveMessageChannelSelection", () => {
params: { cfg: {} as never, channel: "alpha" },
expectedMessage: "Channel is unavailable: alpha",
},
{
setup: () => {
mocks.resolveOutboundChannelPlugin.mockReturnValue(undefined);
mocks.missingOfficialExternalChannels.add("feishu");
},
params: {
cfg: { channels: { feishu: { appId: "cli_xxx" } } } as never,
channel: "feishu",
},
expectedMessage:
"Channel is unavailable: feishu. Install the official external plugin with: openclaw plugins install @openclaw/feishu, or run: openclaw doctor --fix.",
},
{
params: { cfg: {} as never },
expectedMessage: "Channel is required (no configured channels detected).",
},
{
setup: () => {
mocks.resolveOutboundChannelPlugin.mockReturnValue(undefined);
mocks.missingOfficialExternalChannels.add("whatsapp");
},
params: { cfg: { channels: { whatsapp: { enabled: true } } } as never },
expectedMessage:
"Channel is required (no available channels detected). Configured official external channel WhatsApp is missing its plugin. Install the official external plugin with: openclaw plugins install @openclaw/whatsapp, or run: openclaw doctor --fix.",
},
{
setup: () => {
mocks.listChannelPlugins.mockReturnValue([
makePlugin({
id: "whatsapp",
isConfigured: async () => false,
}),
]);
},
params: { cfg: { channels: { whatsapp: { enabled: true } } } as never },
expectedMessage: "Channel is required (no configured channels detected).",
},
{
setup: () => {
mocks.listChannelPlugins.mockReturnValue([

View File

@@ -1,6 +1,10 @@
import { listChannelPlugins } from "../../channels/plugins/index.js";
import type { ChannelPlugin } from "../../channels/plugins/types.plugin.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import {
type OfficialExternalPluginRepairHint,
resolveMissingOfficialExternalChannelPluginRepairHint,
} from "../../plugins/official-external-plugin-repair-hints.js";
import { defaultRuntime } from "../../runtime.js";
import {
listDeliverableMessageChannels,
@@ -53,6 +57,51 @@ function resolveAvailableKnownChannel(params: {
: undefined;
}
function isConfiguredChannel(cfg: OpenClawConfig, channelId: string): boolean {
const channels = cfg.channels;
if (!channels || typeof channels !== "object" || Array.isArray(channels)) {
return false;
}
const entry = (channels as Record<string, unknown>)[channelId];
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
return false;
}
return (entry as { enabled?: unknown }).enabled !== false;
}
function listConfiguredOfficialExternalRepairHints(
cfg: OpenClawConfig,
): OfficialExternalPluginRepairHint[] {
const channels = cfg.channels;
if (!channels || typeof channels !== "object" || Array.isArray(channels)) {
return [];
}
return Object.keys(channels)
.filter((channelId) => isConfiguredChannel(cfg, channelId))
.map((channelId) =>
resolveMissingOfficialExternalChannelPluginRepairHint({
config: cfg,
channelId,
}),
)
.filter((hint): hint is OfficialExternalPluginRepairHint => Boolean(hint));
}
function formatMissingOfficialExternalChannelsMessage(
hints: readonly OfficialExternalPluginRepairHint[],
): string {
if (hints.length === 1) {
const hint = hints[0];
if (!hint) {
return "";
}
return `Configured official external channel ${hint.label} is missing its plugin. ${hint.repairHint}`;
}
const labels = hints.map((hint) => hint.label).join(", ");
const installCommands = hints.map((hint) => hint.installCommand).join("; ");
return `Configured official external channels ${labels} are missing their plugins. Run: openclaw doctor --fix, or install individually: ${installCommands}.`;
}
function isAccountEnabled(account: unknown): boolean {
if (!account || typeof account !== "object") {
return true;
@@ -173,6 +222,15 @@ export async function resolveMessageChannelSelection(params: {
if (!isKnownChannel(normalized)) {
throw new Error(`Unknown channel: ${normalized}`);
}
const repairHint = isConfiguredChannel(params.cfg, normalized)
? resolveMissingOfficialExternalChannelPluginRepairHint({
config: params.cfg,
channelId: normalized,
})
: null;
if (repairHint?.channelId === normalized) {
throw new Error(`Channel is unavailable: ${normalized}. ${repairHint.repairHint}`);
}
throw new Error(`Channel is unavailable: ${normalized}`);
}
return {
@@ -199,6 +257,12 @@ export async function resolveMessageChannelSelection(params: {
return { channel: configured[0], configured, source: "single-configured" };
}
if (configured.length === 0) {
const repairHints = listConfiguredOfficialExternalRepairHints(params.cfg);
if (repairHints.length > 0) {
throw new Error(
`Channel is required (no available channels detected). ${formatMissingOfficialExternalChannelsMessage(repairHints)}`,
);
}
throw new Error("Channel is required (no configured channels detected).");
}
throw new Error(

View File

@@ -0,0 +1,80 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { resolveMissingOfficialExternalChannelPluginRepairHint } from "./official-external-plugin-repair-hints.js";
const mocks = vi.hoisted(() => ({
resolveConfiguredChannelPresencePolicy: vi.fn(),
}));
vi.mock("./channel-plugin-ids.js", () => ({
resolveConfiguredChannelPresencePolicy: (params: unknown) =>
mocks.resolveConfiguredChannelPresencePolicy(params),
}));
describe("resolveMissingOfficialExternalChannelPluginRepairHint", () => {
beforeEach(() => {
mocks.resolveConfiguredChannelPresencePolicy.mockReset();
});
it("returns an install hint when a configured official external channel has no owner", () => {
mocks.resolveConfiguredChannelPresencePolicy.mockReturnValue([
{
channelId: "feishu",
sources: ["explicit-config"],
effective: false,
pluginIds: [],
blockedReasons: ["no-channel-owner"],
},
]);
expect(
resolveMissingOfficialExternalChannelPluginRepairHint({
config: { channels: { feishu: { appId: "cli_xxx" } } },
channelId: "feishu",
}),
).toEqual(
expect.objectContaining({
channelId: "feishu",
installCommand: "openclaw plugins install @openclaw/feishu",
doctorFixCommand: "openclaw doctor --fix",
}),
);
});
it("does not return install hints for policy-blocked official external channel owners", () => {
mocks.resolveConfiguredChannelPresencePolicy.mockReturnValue([
{
channelId: "whatsapp",
sources: ["explicit-config"],
effective: false,
pluginIds: [],
blockedReasons: ["not-in-allowlist"],
},
]);
expect(
resolveMissingOfficialExternalChannelPluginRepairHint({
config: { channels: { whatsapp: { enabled: true } } },
channelId: "whatsapp",
}),
).toBeNull();
});
it("does not return install hints for active official external channel owners", () => {
mocks.resolveConfiguredChannelPresencePolicy.mockReturnValue([
{
channelId: "whatsapp",
sources: ["explicit-config"],
effective: true,
pluginIds: ["whatsapp"],
blockedReasons: [],
},
]);
expect(
resolveMissingOfficialExternalChannelPluginRepairHint({
config: { channels: { whatsapp: { enabled: true } } },
channelId: "whatsapp",
}),
).toBeNull();
});
});

View File

@@ -0,0 +1,77 @@
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { resolveConfiguredChannelPresencePolicy } from "./channel-plugin-ids.js";
import {
getOfficialExternalPluginCatalogEntry,
getOfficialExternalPluginCatalogManifest,
resolveOfficialExternalPluginId,
resolveOfficialExternalPluginInstall,
resolveOfficialExternalPluginLabel,
} from "./official-external-plugin-catalog.js";
export type OfficialExternalPluginRepairHint = {
pluginId: string;
channelId?: string;
label: string;
installSpec: string;
installCommand: string;
doctorFixCommand: string;
repairHint: string;
};
export function resolveOfficialExternalPluginRepairHint(
pluginIdOrChannelId: string,
): OfficialExternalPluginRepairHint | null {
const entry = getOfficialExternalPluginCatalogEntry(pluginIdOrChannelId);
if (!entry) {
return null;
}
const install = resolveOfficialExternalPluginInstall(entry);
const npmSpec = install?.npmSpec?.trim();
const clawhubSpec = install?.clawhubSpec?.trim();
const installSpec =
install?.defaultChoice === "clawhub" ? (clawhubSpec ?? npmSpec) : (npmSpec ?? clawhubSpec);
if (!installSpec) {
return null;
}
const manifest = getOfficialExternalPluginCatalogManifest(entry);
const pluginId = resolveOfficialExternalPluginId(entry) ?? pluginIdOrChannelId.trim();
const channelId = manifest?.channel?.id?.trim();
const label = resolveOfficialExternalPluginLabel(entry);
const installCommand = `openclaw plugins install ${installSpec}`;
const doctorFixCommand = "openclaw doctor --fix";
return {
pluginId,
...(channelId ? { channelId } : {}),
label,
installSpec,
installCommand,
doctorFixCommand,
repairHint: `Install the official external plugin with: ${installCommand}, or run: ${doctorFixCommand}.`,
};
}
export function resolveMissingOfficialExternalChannelPluginRepairHint(params: {
config: OpenClawConfig;
activationSourceConfig?: OpenClawConfig;
channelId: string;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
}): OfficialExternalPluginRepairHint | null {
const hint = resolveOfficialExternalPluginRepairHint(params.channelId);
if (!hint?.channelId || hint.channelId !== params.channelId) {
return null;
}
const policy = resolveConfiguredChannelPresencePolicy({
config: params.config,
activationSourceConfig: params.activationSourceConfig,
workspaceDir: params.workspaceDir,
env: params.env,
includePersistedAuthState: false,
}).find((entry) => entry.channelId === hint.channelId);
if (!policy || policy.effective) {
return null;
}
return policy.blockedReasons.length === 1 && policy.blockedReasons[0] === "no-channel-owner"
? hint
: null;
}