mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-11 01:36:04 +00:00
fix(gateway): hint missing external plugin for web login (#90517)
* fix(gateway): hint missing official plugin for web login * fix(gateway): keep missing web login hints actionable
This commit is contained in:
@@ -7,12 +7,18 @@ import type { GatewayRequestHandlerOptions } from "./types.js";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
listChannelPlugins: vi.fn(),
|
||||
resolveMissingOfficialExternalChannelPluginRepairHint: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../channels/plugins/index.js", () => ({
|
||||
listChannelPlugins: mocks.listChannelPlugins,
|
||||
}));
|
||||
|
||||
vi.mock("../../plugins/official-external-plugin-repair-hints.js", () => ({
|
||||
resolveMissingOfficialExternalChannelPluginRepairHint:
|
||||
mocks.resolveMissingOfficialExternalChannelPluginRepairHint,
|
||||
}));
|
||||
|
||||
import { webHandlers } from "./web.js";
|
||||
|
||||
function createRunningWhatsappSnapshot(): ChannelRuntimeSnapshot {
|
||||
@@ -48,6 +54,7 @@ function createOptions(
|
||||
stopChannel: vi.fn(),
|
||||
startChannel: vi.fn(),
|
||||
getRuntimeSnapshot: vi.fn(createRunningWhatsappSnapshot),
|
||||
getRuntimeConfig: vi.fn(() => ({ channels: { whatsapp: { enabled: true } } })),
|
||||
},
|
||||
...overrides,
|
||||
} as unknown as GatewayRequestHandlerOptions;
|
||||
@@ -63,6 +70,7 @@ function createRunningWhatsappContext() {
|
||||
stopChannel,
|
||||
startChannel,
|
||||
getRuntimeSnapshot: vi.fn(createRunningWhatsappSnapshot),
|
||||
getRuntimeConfig: vi.fn(() => ({ channels: { whatsapp: { enabled: true } } })),
|
||||
} as unknown as GatewayRequestHandlerOptions["context"],
|
||||
};
|
||||
}
|
||||
@@ -70,6 +78,105 @@ function createRunningWhatsappContext() {
|
||||
describe("webHandlers web.login.start", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mocks.resolveMissingOfficialExternalChannelPluginRepairHint.mockReturnValue(null);
|
||||
});
|
||||
|
||||
it("surfaces the missing official external plugin hint when no web-login provider is loaded", async () => {
|
||||
mocks.listChannelPlugins.mockReturnValue([]);
|
||||
mocks.resolveMissingOfficialExternalChannelPluginRepairHint.mockReturnValue({
|
||||
pluginId: "whatsapp",
|
||||
channelId: "whatsapp",
|
||||
label: "WhatsApp",
|
||||
installSpec: "clawhub:@openclaw/whatsapp",
|
||||
installCommand: "openclaw plugins install clawhub:@openclaw/whatsapp",
|
||||
doctorFixCommand: "openclaw doctor --fix",
|
||||
repairHint:
|
||||
"Install the official external plugin with: openclaw plugins install clawhub:@openclaw/whatsapp, or run: openclaw doctor --fix.",
|
||||
});
|
||||
const respond = vi.fn();
|
||||
|
||||
await webHandlers["web.login.start"](
|
||||
createOptions(
|
||||
{ accountId: "default" },
|
||||
{
|
||||
respond,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
expect(respond).toHaveBeenCalledWith(
|
||||
false,
|
||||
undefined,
|
||||
expect.objectContaining({
|
||||
code: "INVALID_REQUEST",
|
||||
message:
|
||||
"web login provider is not available. Install the official external plugin with: openclaw plugins install clawhub:@openclaw/whatsapp, or run: openclaw doctor --fix.",
|
||||
}),
|
||||
);
|
||||
expect(mocks.resolveMissingOfficialExternalChannelPluginRepairHint).toHaveBeenCalledWith({
|
||||
config: { channels: { whatsapp: { enabled: true } } },
|
||||
channelId: "whatsapp",
|
||||
});
|
||||
});
|
||||
|
||||
it("joins multiple missing official external plugin hints when more than one configured channel is missing", async () => {
|
||||
mocks.listChannelPlugins.mockReturnValue([]);
|
||||
mocks.resolveMissingOfficialExternalChannelPluginRepairHint.mockImplementation(({ channelId }) =>
|
||||
channelId === "whatsapp"
|
||||
? {
|
||||
pluginId: "whatsapp",
|
||||
channelId: "whatsapp",
|
||||
label: "WhatsApp",
|
||||
installSpec: "clawhub:@openclaw/whatsapp",
|
||||
installCommand: "openclaw plugins install clawhub:@openclaw/whatsapp",
|
||||
doctorFixCommand: "openclaw doctor --fix",
|
||||
repairHint:
|
||||
"Install the official external plugin with: openclaw plugins install clawhub:@openclaw/whatsapp, or run: openclaw doctor --fix.",
|
||||
}
|
||||
: channelId === "signal"
|
||||
? {
|
||||
pluginId: "signal",
|
||||
channelId: "signal",
|
||||
label: "Signal",
|
||||
installSpec: "clawhub:@openclaw/signal",
|
||||
installCommand: "openclaw plugins install clawhub:@openclaw/signal",
|
||||
doctorFixCommand: "openclaw doctor --fix",
|
||||
repairHint:
|
||||
"Install the official external plugin with: openclaw plugins install clawhub:@openclaw/signal, or run: openclaw doctor --fix.",
|
||||
}
|
||||
: null,
|
||||
);
|
||||
const respond = vi.fn();
|
||||
|
||||
await webHandlers["web.login.start"](
|
||||
createOptions(
|
||||
{ accountId: "default" },
|
||||
{
|
||||
respond,
|
||||
context: {
|
||||
stopChannel: vi.fn(),
|
||||
startChannel: vi.fn(),
|
||||
getRuntimeSnapshot: vi.fn(createRunningWhatsappSnapshot),
|
||||
getRuntimeConfig: vi.fn(() => ({
|
||||
channels: {
|
||||
whatsapp: { enabled: true },
|
||||
signal: { enabled: true },
|
||||
},
|
||||
})),
|
||||
} as unknown as GatewayRequestHandlerOptions["context"],
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
expect(respond).toHaveBeenCalledWith(
|
||||
false,
|
||||
undefined,
|
||||
expect.objectContaining({
|
||||
code: "INVALID_REQUEST",
|
||||
message:
|
||||
"web login provider is not available. Configured official external channel plugins are missing for WhatsApp, Signal. Install them with: openclaw plugins install clawhub:@openclaw/whatsapp; openclaw plugins install clawhub:@openclaw/signal, or run: openclaw doctor --fix.",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
@@ -178,6 +285,7 @@ describe("webHandlers web.login.start", () => {
|
||||
describe("webHandlers web.login.wait", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mocks.resolveMissingOfficialExternalChannelPluginRepairHint.mockReturnValue(null);
|
||||
});
|
||||
|
||||
it("passes refreshed QR payloads back to the client while login is still pending", async () => {
|
||||
|
||||
@@ -8,8 +8,9 @@ import {
|
||||
} from "../../../packages/gateway-protocol/src/index.js";
|
||||
import { listChannelPlugins } from "../../channels/plugins/index.js";
|
||||
import type { ChannelId } from "../../channels/plugins/types.public.js";
|
||||
import { resolveMissingOfficialExternalChannelPluginRepairHint } from "../../plugins/official-external-plugin-repair-hints.js";
|
||||
import { formatForLog } from "../ws-log.js";
|
||||
import type { GatewayRequestHandlers, RespondFn } from "./types.js";
|
||||
import type { GatewayRequestContext, GatewayRequestHandlers, RespondFn } from "./types.js";
|
||||
import { assertValidParams } from "./validation.js";
|
||||
|
||||
const WEB_LOGIN_METHODS = new Set(["web.login.start", "web.login.wait"]);
|
||||
@@ -33,11 +34,44 @@ function resolveAccountId(params: unknown): string | undefined {
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function respondProviderUnavailable(respond: RespondFn) {
|
||||
respond(
|
||||
function resolveMissingWebLoginPluginHint(context: GatewayRequestContext): string | null {
|
||||
const cfg = context.getRuntimeConfig();
|
||||
const channels = cfg.channels;
|
||||
if (!channels || typeof channels !== "object" || Array.isArray(channels)) {
|
||||
return null;
|
||||
}
|
||||
const hints = Object.keys(channels)
|
||||
.map((channelId) =>
|
||||
resolveMissingOfficialExternalChannelPluginRepairHint({
|
||||
config: cfg,
|
||||
channelId,
|
||||
}),
|
||||
)
|
||||
.filter((hint): hint is NonNullable<typeof hint> => Boolean(hint));
|
||||
if (hints.length === 0) {
|
||||
return null;
|
||||
}
|
||||
if (hints.length === 1) {
|
||||
return hints[0].repairHint;
|
||||
}
|
||||
const labels = [...new Set(hints.map((hint) => hint.label))];
|
||||
const installCommands = [...new Set(hints.map((hint) => hint.installCommand))];
|
||||
const doctorFixCommand = hints[0].doctorFixCommand;
|
||||
return `Configured official external channel plugins are missing for ${labels.join(", ")}. Install them with: ${installCommands.join("; ")}, or run: ${doctorFixCommand}.`;
|
||||
}
|
||||
|
||||
function respondProviderUnavailable(params: {
|
||||
respond: RespondFn;
|
||||
context: GatewayRequestContext;
|
||||
}) {
|
||||
const repairHint = resolveMissingWebLoginPluginHint(params.context);
|
||||
const message = repairHint
|
||||
? `web login provider is not available. ${repairHint}`
|
||||
: "web login provider is not available";
|
||||
params.respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.INVALID_REQUEST, "web login provider is not available"),
|
||||
errorShape(ErrorCodes.INVALID_REQUEST, message),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -57,6 +91,7 @@ function respondWebLoginUnavailable(respond: RespondFn, err: unknown) {
|
||||
function resolveWebLoginRequest<TMethod extends WebLoginGatewayMethod>(params: {
|
||||
rawParams: unknown;
|
||||
respond: RespondFn;
|
||||
context: GatewayRequestContext;
|
||||
gatewayMethod: TMethod;
|
||||
}): {
|
||||
accountId?: string;
|
||||
@@ -66,7 +101,10 @@ function resolveWebLoginRequest<TMethod extends WebLoginGatewayMethod>(params: {
|
||||
const accountId = resolveAccountId(params.rawParams);
|
||||
const provider = resolveWebLoginProvider();
|
||||
if (!provider) {
|
||||
respondProviderUnavailable(params.respond);
|
||||
respondProviderUnavailable({
|
||||
respond: params.respond,
|
||||
context: params.context,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
const gateway = provider.gateway;
|
||||
@@ -108,6 +146,7 @@ export const webHandlers: GatewayRequestHandlers = {
|
||||
const request = resolveWebLoginRequest({
|
||||
rawParams: params,
|
||||
respond,
|
||||
context,
|
||||
gatewayMethod: "loginWithQrStart",
|
||||
});
|
||||
if (!request) {
|
||||
@@ -155,6 +194,7 @@ export const webHandlers: GatewayRequestHandlers = {
|
||||
const request = resolveWebLoginRequest({
|
||||
rawParams: params,
|
||||
respond,
|
||||
context,
|
||||
gatewayMethod: "loginWithQrWait",
|
||||
});
|
||||
if (!request) {
|
||||
|
||||
Reference in New Issue
Block a user