mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-18 05:20:48 +00:00
Plugins: broaden plugin surface for Codex App Server (#45318)
* Plugins: add inbound claim and Telegram interaction seams * Plugins: add Discord interaction surface * Chore: fix formatting after plugin rebase * fix(hooks): preserve observers after inbound claim * test(hooks): cover claimed inbound observer delivery * fix(plugins): harden typing lease refreshes * fix(discord): pass real auth to plugin interactions * fix(plugins): remove raw session binding runtime exposure * fix(plugins): tighten interactive callback handling * Plugins: gate conversation binding with approvals * Plugins: migrate legacy plugin binding records * Plugins/phone-control: update test command context * Plugins: migrate legacy binding ids * Plugins: migrate legacy codex session bindings * Discord: fix plugin interaction handling * Discord: support direct plugin conversation binds * Plugins: preserve Discord command bind targets * Tests: fix plugin binding and interactive fallout * Discord: stabilize directory lookup tests * Discord: route bound DMs to plugins * Discord: restore plugin bindings after restart * Telegram: persist detached plugin bindings * Plugins: limit binding APIs to Telegram and Discord * Plugins: harden bound conversation routing * Plugins: fix extension target imports * Plugins: fix Telegram runtime extension imports * Plugins: format rebased binding handlers * Discord: bind group DM interactions by channel --------- Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
@@ -7,7 +7,18 @@ import { monitorDiscordProvider } from "../../../extensions/discord/src/monitor.
|
||||
import { probeDiscord } from "../../../extensions/discord/src/probe.js";
|
||||
import { resolveDiscordChannelAllowlist } from "../../../extensions/discord/src/resolve-channels.js";
|
||||
import { resolveDiscordUserAllowlist } from "../../../extensions/discord/src/resolve-users.js";
|
||||
import { sendMessageDiscord, sendPollDiscord } from "../../../extensions/discord/src/send.js";
|
||||
import {
|
||||
createThreadDiscord,
|
||||
deleteMessageDiscord,
|
||||
editChannelDiscord,
|
||||
editMessageDiscord,
|
||||
pinMessageDiscord,
|
||||
sendDiscordComponentMessage,
|
||||
sendMessageDiscord,
|
||||
sendPollDiscord,
|
||||
sendTypingDiscord,
|
||||
unpinMessageDiscord,
|
||||
} from "../../../extensions/discord/src/send.js";
|
||||
import { monitorIMessageProvider } from "../../../extensions/imessage/src/monitor.js";
|
||||
import { probeIMessage } from "../../../extensions/imessage/src/probe.js";
|
||||
import { sendMessageIMessage } from "../../../extensions/imessage/src/send.js";
|
||||
@@ -29,7 +40,17 @@ import {
|
||||
} from "../../../extensions/telegram/src/audit.js";
|
||||
import { monitorTelegramProvider } from "../../../extensions/telegram/src/monitor.js";
|
||||
import { probeTelegram } from "../../../extensions/telegram/src/probe.js";
|
||||
import { sendMessageTelegram, sendPollTelegram } from "../../../extensions/telegram/src/send.js";
|
||||
import {
|
||||
deleteMessageTelegram,
|
||||
editMessageReplyMarkupTelegram,
|
||||
editMessageTelegram,
|
||||
pinMessageTelegram,
|
||||
renameForumTopicTelegram,
|
||||
sendMessageTelegram,
|
||||
sendPollTelegram,
|
||||
sendTypingTelegram,
|
||||
unpinMessageTelegram,
|
||||
} from "../../../extensions/telegram/src/send.js";
|
||||
import { resolveTelegramToken } from "../../../extensions/telegram/src/token.js";
|
||||
import { resolveEffectiveMessagesConfig, resolveHumanDelayConfig } from "../../agents/identity.js";
|
||||
import { handleSlackAction } from "../../agents/tools/slack-actions.js";
|
||||
@@ -113,6 +134,8 @@ import {
|
||||
upsertChannelPairingRequest,
|
||||
} from "../../pairing/pairing-store.js";
|
||||
import { buildAgentSessionKey, resolveAgentRoute } from "../../routing/resolve-route.js";
|
||||
import { createDiscordTypingLease } from "./runtime-discord-typing.js";
|
||||
import { createTelegramTypingLease } from "./runtime-telegram-typing.js";
|
||||
import { createRuntimeWhatsApp } from "./runtime-whatsapp.js";
|
||||
import type { PluginRuntime } from "./types.js";
|
||||
|
||||
@@ -207,9 +230,33 @@ export function createRuntimeChannel(): PluginRuntime["channel"] {
|
||||
probeDiscord,
|
||||
resolveChannelAllowlist: resolveDiscordChannelAllowlist,
|
||||
resolveUserAllowlist: resolveDiscordUserAllowlist,
|
||||
sendComponentMessage: sendDiscordComponentMessage,
|
||||
sendMessageDiscord,
|
||||
sendPollDiscord,
|
||||
monitorDiscordProvider,
|
||||
typing: {
|
||||
pulse: sendTypingDiscord,
|
||||
start: async ({ channelId, accountId, cfg, intervalMs }) =>
|
||||
await createDiscordTypingLease({
|
||||
channelId,
|
||||
accountId,
|
||||
cfg,
|
||||
intervalMs,
|
||||
pulse: async ({ channelId, accountId, cfg }) =>
|
||||
void (await sendTypingDiscord(channelId, {
|
||||
accountId,
|
||||
cfg,
|
||||
})),
|
||||
}),
|
||||
},
|
||||
conversationActions: {
|
||||
editMessage: editMessageDiscord,
|
||||
deleteMessage: deleteMessageDiscord,
|
||||
pinMessage: pinMessageDiscord,
|
||||
unpinMessage: unpinMessageDiscord,
|
||||
createThread: createThreadDiscord,
|
||||
editChannel: editChannelDiscord,
|
||||
},
|
||||
},
|
||||
slack: {
|
||||
listDirectoryGroupsLive: listSlackDirectoryGroupsLive,
|
||||
@@ -230,6 +277,33 @@ export function createRuntimeChannel(): PluginRuntime["channel"] {
|
||||
sendPollTelegram,
|
||||
monitorTelegramProvider,
|
||||
messageActions: telegramMessageActions,
|
||||
typing: {
|
||||
pulse: sendTypingTelegram,
|
||||
start: async ({ to, accountId, cfg, intervalMs, messageThreadId }) =>
|
||||
await createTelegramTypingLease({
|
||||
to,
|
||||
accountId,
|
||||
cfg,
|
||||
intervalMs,
|
||||
messageThreadId,
|
||||
pulse: async ({ to, accountId, cfg, messageThreadId }) =>
|
||||
await sendTypingTelegram(to, {
|
||||
accountId,
|
||||
cfg,
|
||||
messageThreadId,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
conversationActions: {
|
||||
editMessage: editMessageTelegram,
|
||||
editReplyMarkup: editMessageReplyMarkupTelegram,
|
||||
clearReplyMarkup: async (chatIdInput, messageIdInput, opts = {}) =>
|
||||
await editMessageReplyMarkupTelegram(chatIdInput, messageIdInput, [], opts),
|
||||
deleteMessage: deleteMessageTelegram,
|
||||
renameTopic: renameForumTopicTelegram,
|
||||
pinMessage: pinMessageTelegram,
|
||||
unpinMessage: unpinMessageTelegram,
|
||||
},
|
||||
},
|
||||
signal: {
|
||||
probeSignal,
|
||||
|
||||
57
src/plugins/runtime/runtime-discord-typing.test.ts
Normal file
57
src/plugins/runtime/runtime-discord-typing.test.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { createDiscordTypingLease } from "./runtime-discord-typing.js";
|
||||
|
||||
describe("createDiscordTypingLease", () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("pulses immediately and keeps leases independent", async () => {
|
||||
vi.useFakeTimers();
|
||||
const pulse = vi.fn(async () => undefined);
|
||||
|
||||
const leaseA = await createDiscordTypingLease({
|
||||
channelId: "123",
|
||||
intervalMs: 2_000,
|
||||
pulse,
|
||||
});
|
||||
const leaseB = await createDiscordTypingLease({
|
||||
channelId: "123",
|
||||
intervalMs: 2_000,
|
||||
pulse,
|
||||
});
|
||||
|
||||
expect(pulse).toHaveBeenCalledTimes(2);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(2_000);
|
||||
expect(pulse).toHaveBeenCalledTimes(4);
|
||||
|
||||
leaseA.stop();
|
||||
await vi.advanceTimersByTimeAsync(2_000);
|
||||
expect(pulse).toHaveBeenCalledTimes(5);
|
||||
|
||||
await leaseB.refresh();
|
||||
expect(pulse).toHaveBeenCalledTimes(6);
|
||||
|
||||
leaseB.stop();
|
||||
});
|
||||
|
||||
it("swallows background pulse failures", async () => {
|
||||
vi.useFakeTimers();
|
||||
const pulse = vi
|
||||
.fn<(params: { channelId: string; accountId?: string; cfg?: unknown }) => Promise<void>>()
|
||||
.mockResolvedValueOnce(undefined)
|
||||
.mockRejectedValueOnce(new Error("boom"));
|
||||
|
||||
const lease = await createDiscordTypingLease({
|
||||
channelId: "123",
|
||||
intervalMs: 2_000,
|
||||
pulse,
|
||||
});
|
||||
|
||||
await expect(vi.advanceTimersByTimeAsync(2_000)).resolves.toBe(vi);
|
||||
expect(pulse).toHaveBeenCalledTimes(2);
|
||||
|
||||
lease.stop();
|
||||
});
|
||||
});
|
||||
62
src/plugins/runtime/runtime-discord-typing.ts
Normal file
62
src/plugins/runtime/runtime-discord-typing.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { logWarn } from "../../logger.js";
|
||||
|
||||
export type CreateDiscordTypingLeaseParams = {
|
||||
channelId: string;
|
||||
accountId?: string;
|
||||
cfg?: ReturnType<typeof import("../../config/config.js").loadConfig>;
|
||||
intervalMs?: number;
|
||||
pulse: (params: {
|
||||
channelId: string;
|
||||
accountId?: string;
|
||||
cfg?: ReturnType<typeof import("../../config/config.js").loadConfig>;
|
||||
}) => Promise<void>;
|
||||
};
|
||||
|
||||
const DEFAULT_DISCORD_TYPING_INTERVAL_MS = 8_000;
|
||||
|
||||
export async function createDiscordTypingLease(params: CreateDiscordTypingLeaseParams): Promise<{
|
||||
refresh: () => Promise<void>;
|
||||
stop: () => void;
|
||||
}> {
|
||||
const intervalMs =
|
||||
typeof params.intervalMs === "number" && Number.isFinite(params.intervalMs)
|
||||
? Math.max(1_000, Math.floor(params.intervalMs))
|
||||
: DEFAULT_DISCORD_TYPING_INTERVAL_MS;
|
||||
|
||||
let stopped = false;
|
||||
let timer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
const pulse = async () => {
|
||||
if (stopped) {
|
||||
return;
|
||||
}
|
||||
await params.pulse({
|
||||
channelId: params.channelId,
|
||||
accountId: params.accountId,
|
||||
cfg: params.cfg,
|
||||
});
|
||||
};
|
||||
|
||||
await pulse();
|
||||
|
||||
timer = setInterval(() => {
|
||||
// Background lease refreshes must never escape as unhandled rejections.
|
||||
void pulse().catch((err) => {
|
||||
logWarn(`plugins: discord typing pulse failed: ${String(err)}`);
|
||||
});
|
||||
}, intervalMs);
|
||||
timer.unref?.();
|
||||
|
||||
return {
|
||||
refresh: async () => {
|
||||
await pulse();
|
||||
},
|
||||
stop: () => {
|
||||
stopped = true;
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
83
src/plugins/runtime/runtime-telegram-typing.test.ts
Normal file
83
src/plugins/runtime/runtime-telegram-typing.test.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { createTelegramTypingLease } from "./runtime-telegram-typing.js";
|
||||
|
||||
describe("createTelegramTypingLease", () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("pulses immediately and keeps leases independent", async () => {
|
||||
vi.useFakeTimers();
|
||||
const pulse = vi.fn(async () => undefined);
|
||||
|
||||
const leaseA = await createTelegramTypingLease({
|
||||
to: "telegram:123",
|
||||
intervalMs: 2_000,
|
||||
pulse,
|
||||
});
|
||||
const leaseB = await createTelegramTypingLease({
|
||||
to: "telegram:123",
|
||||
intervalMs: 2_000,
|
||||
pulse,
|
||||
});
|
||||
|
||||
expect(pulse).toHaveBeenCalledTimes(2);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(2_000);
|
||||
expect(pulse).toHaveBeenCalledTimes(4);
|
||||
|
||||
leaseA.stop();
|
||||
await vi.advanceTimersByTimeAsync(2_000);
|
||||
expect(pulse).toHaveBeenCalledTimes(5);
|
||||
|
||||
await leaseB.refresh();
|
||||
expect(pulse).toHaveBeenCalledTimes(6);
|
||||
|
||||
leaseB.stop();
|
||||
});
|
||||
|
||||
it("swallows background pulse failures", async () => {
|
||||
vi.useFakeTimers();
|
||||
const pulse = vi
|
||||
.fn<
|
||||
(params: {
|
||||
to: string;
|
||||
accountId?: string;
|
||||
cfg?: unknown;
|
||||
messageThreadId?: number;
|
||||
}) => Promise<unknown>
|
||||
>()
|
||||
.mockResolvedValueOnce(undefined)
|
||||
.mockRejectedValueOnce(new Error("boom"));
|
||||
|
||||
const lease = await createTelegramTypingLease({
|
||||
to: "telegram:123",
|
||||
intervalMs: 2_000,
|
||||
pulse,
|
||||
});
|
||||
|
||||
await expect(vi.advanceTimersByTimeAsync(2_000)).resolves.toBe(vi);
|
||||
expect(pulse).toHaveBeenCalledTimes(2);
|
||||
|
||||
lease.stop();
|
||||
});
|
||||
|
||||
it("falls back to the default interval for non-finite values", async () => {
|
||||
vi.useFakeTimers();
|
||||
const pulse = vi.fn(async () => undefined);
|
||||
|
||||
const lease = await createTelegramTypingLease({
|
||||
to: "telegram:123",
|
||||
intervalMs: Number.NaN,
|
||||
pulse,
|
||||
});
|
||||
|
||||
expect(pulse).toHaveBeenCalledTimes(1);
|
||||
await vi.advanceTimersByTimeAsync(3_999);
|
||||
expect(pulse).toHaveBeenCalledTimes(1);
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
expect(pulse).toHaveBeenCalledTimes(2);
|
||||
|
||||
lease.stop();
|
||||
});
|
||||
});
|
||||
60
src/plugins/runtime/runtime-telegram-typing.ts
Normal file
60
src/plugins/runtime/runtime-telegram-typing.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { logWarn } from "../../logger.js";
|
||||
|
||||
export type CreateTelegramTypingLeaseParams = {
|
||||
to: string;
|
||||
accountId?: string;
|
||||
cfg?: OpenClawConfig;
|
||||
intervalMs?: number;
|
||||
messageThreadId?: number;
|
||||
pulse: (params: {
|
||||
to: string;
|
||||
accountId?: string;
|
||||
cfg?: OpenClawConfig;
|
||||
messageThreadId?: number;
|
||||
}) => Promise<unknown>;
|
||||
};
|
||||
|
||||
export async function createTelegramTypingLease(params: CreateTelegramTypingLeaseParams): Promise<{
|
||||
refresh: () => Promise<void>;
|
||||
stop: () => void;
|
||||
}> {
|
||||
const intervalMs =
|
||||
typeof params.intervalMs === "number" && Number.isFinite(params.intervalMs)
|
||||
? Math.max(1_000, Math.floor(params.intervalMs))
|
||||
: 4_000;
|
||||
let stopped = false;
|
||||
|
||||
const refresh = async () => {
|
||||
if (stopped) {
|
||||
return;
|
||||
}
|
||||
await params.pulse({
|
||||
to: params.to,
|
||||
accountId: params.accountId,
|
||||
cfg: params.cfg,
|
||||
messageThreadId: params.messageThreadId,
|
||||
});
|
||||
};
|
||||
|
||||
await refresh();
|
||||
|
||||
const timer = setInterval(() => {
|
||||
// Background lease refreshes must never escape as unhandled rejections.
|
||||
void refresh().catch((err) => {
|
||||
logWarn(`plugins: telegram typing pulse failed: ${String(err)}`);
|
||||
});
|
||||
}, intervalMs);
|
||||
timer.unref?.();
|
||||
|
||||
return {
|
||||
refresh,
|
||||
stop: () => {
|
||||
if (stopped) {
|
||||
return;
|
||||
}
|
||||
stopped = true;
|
||||
clearInterval(timer);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -94,9 +94,30 @@ export type PluginRuntimeChannel = {
|
||||
probeDiscord: typeof import("../../../extensions/discord/src/probe.js").probeDiscord;
|
||||
resolveChannelAllowlist: typeof import("../../../extensions/discord/src/resolve-channels.js").resolveDiscordChannelAllowlist;
|
||||
resolveUserAllowlist: typeof import("../../../extensions/discord/src/resolve-users.js").resolveDiscordUserAllowlist;
|
||||
sendComponentMessage: typeof import("../../../extensions/discord/src/send.js").sendDiscordComponentMessage;
|
||||
sendMessageDiscord: typeof import("../../../extensions/discord/src/send.js").sendMessageDiscord;
|
||||
sendPollDiscord: typeof import("../../../extensions/discord/src/send.js").sendPollDiscord;
|
||||
monitorDiscordProvider: typeof import("../../../extensions/discord/src/monitor.js").monitorDiscordProvider;
|
||||
typing: {
|
||||
pulse: typeof import("../../../extensions/discord/src/send.js").sendTypingDiscord;
|
||||
start: (params: {
|
||||
channelId: string;
|
||||
accountId?: string;
|
||||
cfg?: ReturnType<typeof import("../../config/config.js").loadConfig>;
|
||||
intervalMs?: number;
|
||||
}) => Promise<{
|
||||
refresh: () => Promise<void>;
|
||||
stop: () => void;
|
||||
}>;
|
||||
};
|
||||
conversationActions: {
|
||||
editMessage: typeof import("../../../extensions/discord/src/send.js").editMessageDiscord;
|
||||
deleteMessage: typeof import("../../../extensions/discord/src/send.js").deleteMessageDiscord;
|
||||
pinMessage: typeof import("../../../extensions/discord/src/send.js").pinMessageDiscord;
|
||||
unpinMessage: typeof import("../../../extensions/discord/src/send.js").unpinMessageDiscord;
|
||||
createThread: typeof import("../../../extensions/discord/src/send.js").createThreadDiscord;
|
||||
editChannel: typeof import("../../../extensions/discord/src/send.js").editChannelDiscord;
|
||||
};
|
||||
};
|
||||
slack: {
|
||||
listDirectoryGroupsLive: typeof import("../../../extensions/slack/src/directory-live.js").listSlackDirectoryGroupsLive;
|
||||
@@ -117,6 +138,39 @@ export type PluginRuntimeChannel = {
|
||||
sendPollTelegram: typeof import("../../../extensions/telegram/src/send.js").sendPollTelegram;
|
||||
monitorTelegramProvider: typeof import("../../../extensions/telegram/src/monitor.js").monitorTelegramProvider;
|
||||
messageActions: typeof import("../../channels/plugins/actions/telegram.js").telegramMessageActions;
|
||||
typing: {
|
||||
pulse: typeof import("../../../extensions/telegram/src/send.js").sendTypingTelegram;
|
||||
start: (params: {
|
||||
to: string;
|
||||
accountId?: string;
|
||||
cfg?: ReturnType<typeof import("../../config/config.js").loadConfig>;
|
||||
intervalMs?: number;
|
||||
messageThreadId?: number;
|
||||
}) => Promise<{
|
||||
refresh: () => Promise<void>;
|
||||
stop: () => void;
|
||||
}>;
|
||||
};
|
||||
conversationActions: {
|
||||
editMessage: typeof import("../../../extensions/telegram/src/send.js").editMessageTelegram;
|
||||
editReplyMarkup: typeof import("../../../extensions/telegram/src/send.js").editMessageReplyMarkupTelegram;
|
||||
clearReplyMarkup: (
|
||||
chatIdInput: string | number,
|
||||
messageIdInput: string | number,
|
||||
opts?: {
|
||||
token?: string;
|
||||
accountId?: string;
|
||||
verbose?: boolean;
|
||||
api?: Partial<import("grammy").Bot["api"]>;
|
||||
retry?: import("../../infra/retry.js").RetryConfig;
|
||||
cfg?: ReturnType<typeof import("../../config/config.js").loadConfig>;
|
||||
},
|
||||
) => Promise<{ ok: true; messageId: string; chatId: string }>;
|
||||
deleteMessage: typeof import("../../../extensions/telegram/src/send.js").deleteMessageTelegram;
|
||||
renameTopic: typeof import("../../../extensions/telegram/src/send.js").renameForumTopicTelegram;
|
||||
pinMessage: typeof import("../../../extensions/telegram/src/send.js").pinMessageTelegram;
|
||||
unpinMessage: typeof import("../../../extensions/telegram/src/send.js").unpinMessageTelegram;
|
||||
};
|
||||
};
|
||||
signal: {
|
||||
probeSignal: typeof import("../../../extensions/signal/src/probe.js").probeSignal;
|
||||
|
||||
Reference in New Issue
Block a user