mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 14:20:44 +00:00
fix(discord): avoid blocking startup on probe (#77129)
* fix(discord): avoid blocking startup on probe * fix(discord): clear degraded probe status * test(plugin-sdk): isolate jiti loader override * test(plugin-sdk): fix circular facade fixture path * fix(plugins): preserve sdk aliases for native loads * fix(plugins): route sdk alias loads through transform
This commit is contained in:
committed by
GitHub
parent
fa689295c6
commit
605e89468e
@@ -379,7 +379,7 @@ describe("discordPlugin outbound", () => {
|
||||
expect(runtimeProbeDiscord).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses direct Discord startup helpers before monitoring", async () => {
|
||||
it("uses direct Discord startup helpers for async startup enrichment", async () => {
|
||||
const runtimeProbeDiscord = vi.fn(async () => {
|
||||
throw new Error("runtime Discord probe should not be used");
|
||||
});
|
||||
@@ -407,9 +407,11 @@ describe("discordPlugin outbound", () => {
|
||||
const cfg = createCfg();
|
||||
await startDiscordAccount(cfg);
|
||||
|
||||
expect(probeDiscordMock).toHaveBeenCalledWith("discord-token", 2500, {
|
||||
includeApplication: true,
|
||||
});
|
||||
await vi.waitFor(() =>
|
||||
expect(probeDiscordMock).toHaveBeenCalledWith("discord-token", 2500, {
|
||||
includeApplication: true,
|
||||
}),
|
||||
);
|
||||
expect(monitorDiscordProviderMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
token: "discord-token",
|
||||
@@ -421,6 +423,98 @@ describe("discordPlugin outbound", () => {
|
||||
expect(runtimeMonitorDiscordProvider).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not block Discord monitor startup on the startup probe", async () => {
|
||||
let resolveProbe!: (value: {
|
||||
ok: true;
|
||||
bot: { username: string };
|
||||
application: { intents: { messageContent: "limited" } };
|
||||
elapsedMs: number;
|
||||
}) => void;
|
||||
probeDiscordMock.mockReturnValue(
|
||||
new Promise((resolve) => {
|
||||
resolveProbe = resolve;
|
||||
}),
|
||||
);
|
||||
monitorDiscordProviderMock.mockResolvedValue(undefined);
|
||||
|
||||
const cfg = createCfg();
|
||||
const statusPatches: Array<Record<string, unknown>> = [];
|
||||
const ctx = createStartAccountContext({
|
||||
account: resolveAccount(cfg),
|
||||
cfg,
|
||||
statusPatchSink: (next) => statusPatches.push({ ...next }),
|
||||
});
|
||||
|
||||
await discordPlugin.gateway!.startAccount!(ctx);
|
||||
|
||||
expect(monitorDiscordProviderMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
token: "discord-token",
|
||||
accountId: "default",
|
||||
}),
|
||||
);
|
||||
await vi.waitFor(() =>
|
||||
expect(probeDiscordMock).toHaveBeenCalledWith("discord-token", 2500, {
|
||||
includeApplication: true,
|
||||
}),
|
||||
);
|
||||
expect(statusPatches.some((patch) => "bot" in patch || "application" in patch)).toBe(false);
|
||||
|
||||
resolveProbe({
|
||||
ok: true,
|
||||
bot: { username: "AsyncBob" },
|
||||
application: { intents: { messageContent: "limited" } },
|
||||
elapsedMs: 1,
|
||||
});
|
||||
|
||||
await vi.waitFor(() =>
|
||||
expect(
|
||||
statusPatches.some(
|
||||
(patch) =>
|
||||
(patch.bot as { username?: string } | undefined)?.username === "AsyncBob" &&
|
||||
Boolean(patch.application),
|
||||
),
|
||||
).toBe(true),
|
||||
);
|
||||
});
|
||||
|
||||
it("clears stale Discord probe metadata when the async startup probe degrades", async () => {
|
||||
probeDiscordMock.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 401,
|
||||
error: "getMe failed (401)",
|
||||
elapsedMs: 1,
|
||||
});
|
||||
monitorDiscordProviderMock.mockResolvedValue(undefined);
|
||||
|
||||
const cfg = createCfg();
|
||||
const statusPatches: Array<Record<string, unknown>> = [];
|
||||
const ctx = createStartAccountContext({
|
||||
account: resolveAccount(cfg),
|
||||
cfg,
|
||||
statusPatchSink: (next) => statusPatches.push({ ...next }),
|
||||
});
|
||||
ctx.setStatus({
|
||||
accountId: "default",
|
||||
bot: { username: "OldBot" },
|
||||
application: { intents: { messageContent: "enabled" } },
|
||||
});
|
||||
|
||||
await discordPlugin.gateway!.startAccount!(ctx);
|
||||
|
||||
await vi.waitFor(() =>
|
||||
expect(
|
||||
statusPatches.some(
|
||||
(patch) =>
|
||||
"bot" in patch &&
|
||||
"application" in patch &&
|
||||
patch.bot === undefined &&
|
||||
patch.application === undefined,
|
||||
),
|
||||
).toBe(true),
|
||||
);
|
||||
});
|
||||
|
||||
it("stagger starts later accounts in multi-bot setups", async () => {
|
||||
probeDiscordMock.mockResolvedValue({
|
||||
ok: true,
|
||||
|
||||
@@ -82,6 +82,61 @@ import { parseDiscordTarget } from "./target-parsing.js";
|
||||
const REQUIRED_DISCORD_PERMISSIONS = ["ViewChannel", "SendMessages"] as const;
|
||||
const DISCORD_ACCOUNT_STARTUP_STAGGER_MS = 10_000;
|
||||
|
||||
function startDiscordStartupProbe(params: {
|
||||
accountId: string;
|
||||
token: string;
|
||||
abortSignal: AbortSignal;
|
||||
setStatus: (patch: { accountId: string; bot?: unknown; application?: unknown }) => void;
|
||||
log?: {
|
||||
warn?: (msg: string) => void;
|
||||
info?: (msg: string) => void;
|
||||
debug?: (msg: string) => void;
|
||||
};
|
||||
}): void {
|
||||
void (async () => {
|
||||
try {
|
||||
const probe = await (
|
||||
await loadDiscordProbeRuntime()
|
||||
).probeDiscord(params.token, 2500, {
|
||||
includeApplication: true,
|
||||
});
|
||||
if (params.abortSignal.aborted) {
|
||||
return;
|
||||
}
|
||||
params.setStatus({
|
||||
accountId: params.accountId,
|
||||
bot: probe.bot,
|
||||
application: probe.application,
|
||||
});
|
||||
if (probe.ok) {
|
||||
const username = probe.bot?.username?.trim();
|
||||
if (username) {
|
||||
params.log?.info?.(`[${params.accountId}] Discord bot probe resolved @${username}`);
|
||||
}
|
||||
} else if (getDiscordRuntime().logging.shouldLogVerbose()) {
|
||||
params.log?.debug?.(
|
||||
`[${params.accountId}] bot probe degraded: ${probe.error ?? `status ${probe.status ?? "unknown"}`}`,
|
||||
);
|
||||
}
|
||||
|
||||
const messageContent = probe.application?.intents?.messageContent;
|
||||
if (messageContent === "disabled") {
|
||||
params.log?.warn?.(
|
||||
`[${params.accountId}] Discord Message Content Intent is disabled; bot may not respond to channel messages. Enable it in Discord Dev Portal (Bot → Privileged Gateway Intents) or require mentions.`,
|
||||
);
|
||||
} else if (messageContent === "limited") {
|
||||
params.log?.info?.(
|
||||
`[${params.accountId}] Discord Message Content Intent is limited; bots under 100 servers can use it without verification.`,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
if (getDiscordRuntime().logging.shouldLogVerbose()) {
|
||||
params.log?.debug?.(`[${params.accountId}] bot probe failed: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
function shouldTreatDiscordDeliveredTextAsVisible(params: {
|
||||
kind: "tool" | "block" | "final";
|
||||
text?: string;
|
||||
@@ -551,38 +606,14 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount, DiscordProbe>
|
||||
}
|
||||
}
|
||||
const token = account.token.trim();
|
||||
let discordBotLabel = "";
|
||||
try {
|
||||
const probe = await (
|
||||
await loadDiscordProbeRuntime()
|
||||
).probeDiscord(token, 2500, {
|
||||
includeApplication: true,
|
||||
});
|
||||
const username = probe.ok ? probe.bot?.username?.trim() : null;
|
||||
if (username) {
|
||||
discordBotLabel = ` (@${username})`;
|
||||
}
|
||||
ctx.setStatus({
|
||||
accountId: account.accountId,
|
||||
bot: probe.bot,
|
||||
application: probe.application,
|
||||
});
|
||||
const messageContent = probe.application?.intents?.messageContent;
|
||||
if (messageContent === "disabled") {
|
||||
ctx.log?.warn(
|
||||
`[${account.accountId}] Discord Message Content Intent is disabled; bot may not respond to channel messages. Enable it in Discord Dev Portal (Bot → Privileged Gateway Intents) or require mentions.`,
|
||||
);
|
||||
} else if (messageContent === "limited") {
|
||||
ctx.log?.info(
|
||||
`[${account.accountId}] Discord Message Content Intent is limited; bots under 100 servers can use it without verification.`,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
if (getDiscordRuntime().logging.shouldLogVerbose()) {
|
||||
ctx.log?.debug?.(`[${account.accountId}] bot probe failed: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
ctx.log?.info(`[${account.accountId}] starting provider${discordBotLabel}`);
|
||||
startDiscordStartupProbe({
|
||||
accountId: account.accountId,
|
||||
token,
|
||||
abortSignal: ctx.abortSignal,
|
||||
setStatus: ctx.setStatus,
|
||||
log: ctx.log,
|
||||
});
|
||||
ctx.log?.info(`[${account.accountId}] starting provider`);
|
||||
return (await loadDiscordProviderRuntime()).monitorDiscordProvider({
|
||||
token,
|
||||
accountId: account.accountId,
|
||||
|
||||
Reference in New Issue
Block a user