mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-10 08:41:13 +00:00
refactor(plugin-sdk): genericize web channel runtime seams
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -906,7 +906,7 @@ const { enqueueSystemEventSpy, resolveAgentRouteMock } = vi.hoisted(() => ({
|
||||
})),
|
||||
}));
|
||||
|
||||
const channelRuntimeModule = await import("openclaw/plugin-sdk/channel-runtime");
|
||||
const channelRuntimeModule = await import("openclaw/plugin-sdk/infra-runtime");
|
||||
vi.spyOn(channelRuntimeModule, "enqueueSystemEvent").mockImplementation(enqueueSystemEventSpy);
|
||||
|
||||
const routingModule = await import("openclaw/plugin-sdk/routing");
|
||||
|
||||
@@ -23,10 +23,10 @@ import {
|
||||
formatInboundEnvelope,
|
||||
resolveEnvelopeFormatOptions,
|
||||
} from "openclaw/plugin-sdk/channel-inbound";
|
||||
import { enqueueSystemEvent } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import type { DiscordAccountConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-name-runtime";
|
||||
import { enqueueSystemEvent } from "openclaw/plugin-sdk/infra-runtime";
|
||||
import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime";
|
||||
import { getAgentScopedMediaLocalRoots } from "openclaw/plugin-sdk/media-runtime";
|
||||
import { type PluginInteractiveDiscordHandlerContext } from "openclaw/plugin-sdk/plugin-runtime";
|
||||
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
ThreadUpdateListener,
|
||||
type User,
|
||||
} from "@buape/carbon";
|
||||
import { enqueueSystemEvent } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { enqueueSystemEvent } from "openclaw/plugin-sdk/infra-runtime";
|
||||
import { resolveAgentRoute } from "openclaw/plugin-sdk/routing";
|
||||
import {
|
||||
createSubsystemLogger,
|
||||
|
||||
@@ -7,13 +7,13 @@ import {
|
||||
matchesMentionWithExplicit,
|
||||
resolveMentionGatingWithBypass,
|
||||
} from "openclaw/plugin-sdk/channel-inbound";
|
||||
import { enqueueSystemEvent, recordChannelActivity } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import { resolveControlCommandGate } from "openclaw/plugin-sdk/command-auth-native";
|
||||
import { hasControlCommand } from "openclaw/plugin-sdk/command-detection";
|
||||
import { shouldHandleTextCommands } from "openclaw/plugin-sdk/command-surface";
|
||||
import { loadConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/config-runtime";
|
||||
import type { SessionBindingRecord } from "openclaw/plugin-sdk/conversation-runtime";
|
||||
import { enqueueSystemEvent, recordChannelActivity } from "openclaw/plugin-sdk/infra-runtime";
|
||||
import {
|
||||
recordPendingHistoryEntryIfEnabled,
|
||||
type HistoryEntry,
|
||||
@@ -289,8 +289,9 @@ export async function preflightDiscordMessage(
|
||||
const pluralkitConfig = params.discordConfig?.pluralkit;
|
||||
const webhookId = resolveDiscordWebhookId(message);
|
||||
const shouldCheckPluralKit = Boolean(pluralkitConfig?.enabled) && !webhookId;
|
||||
let pluralkitInfo: Awaited<ReturnType<typeof import("../pluralkit.js").fetchPluralKitMessageInfo>> =
|
||||
null;
|
||||
let pluralkitInfo: Awaited<
|
||||
ReturnType<typeof import("../pluralkit.js").fetchPluralKitMessageInfo>
|
||||
> = null;
|
||||
if (shouldCheckPluralKit) {
|
||||
try {
|
||||
const { fetchPluralKitMessageInfo } = await loadPluralKitRuntime();
|
||||
|
||||
@@ -65,7 +65,7 @@ const dispatchPluginInteractiveHandlerMock = vi.hoisted(() => vi.fn());
|
||||
let lastDispatchCtx: Record<string, unknown> | undefined;
|
||||
|
||||
async function createChannelRuntimeMock(
|
||||
importOriginal: () => Promise<typeof import("openclaw/plugin-sdk/channel-runtime")>,
|
||||
importOriginal: () => Promise<typeof import("openclaw/plugin-sdk/infra-runtime")>,
|
||||
) {
|
||||
const actual = await importOriginal();
|
||||
return {
|
||||
@@ -74,8 +74,8 @@ async function createChannelRuntimeMock(
|
||||
};
|
||||
}
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/channel-runtime", createChannelRuntimeMock);
|
||||
vi.mock("openclaw/plugin-sdk/channel-runtime.js", createChannelRuntimeMock);
|
||||
vi.mock("openclaw/plugin-sdk/infra-runtime", createChannelRuntimeMock);
|
||||
vi.mock("openclaw/plugin-sdk/infra-runtime.js", createChannelRuntimeMock);
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/reply-runtime", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/reply-runtime")>();
|
||||
|
||||
@@ -5,8 +5,8 @@ import {
|
||||
type RequestClient,
|
||||
} from "@buape/carbon";
|
||||
import { ChannelType, Routes } from "discord-api-types/v10";
|
||||
import { recordChannelActivity } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import { loadConfig, type OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { recordChannelActivity } from "openclaw/plugin-sdk/infra-runtime";
|
||||
import { resolveDiscordAccount } from "./accounts.js";
|
||||
import { registerDiscordComponentEntries } from "./components-registry.js";
|
||||
import {
|
||||
|
||||
@@ -3,9 +3,9 @@ import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { serializePayload, type MessagePayloadObject, type RequestClient } from "@buape/carbon";
|
||||
import { ChannelType, Routes } from "discord-api-types/v10";
|
||||
import { recordChannelActivity } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import { loadConfig, type OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { recordChannelActivity } from "openclaw/plugin-sdk/infra-runtime";
|
||||
import { maxBytesForKind } from "openclaw/plugin-sdk/media-runtime";
|
||||
import { extensionForMime } from "openclaw/plugin-sdk/media-runtime";
|
||||
import { unlinkIfExists } from "openclaw/plugin-sdk/media-runtime";
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
} from "openclaw/plugin-sdk/channel-inbound";
|
||||
import { createChannelPairingChallengeIssuer } from "openclaw/plugin-sdk/channel-pairing";
|
||||
import { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline";
|
||||
import { waitForTransportReady } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import { loadConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import {
|
||||
resolveOpenProviderRuntimeGroupPolicy,
|
||||
@@ -20,6 +19,7 @@ import {
|
||||
} from "openclaw/plugin-sdk/conversation-runtime";
|
||||
import { recordInboundSession } from "openclaw/plugin-sdk/conversation-runtime";
|
||||
import { normalizeScpRemoteHost } from "openclaw/plugin-sdk/host-runtime";
|
||||
import { waitForTransportReady } from "openclaw/plugin-sdk/infra-runtime";
|
||||
import {
|
||||
isInboundPathAllowed,
|
||||
resolveIMessageAttachmentRoots,
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
resolveInboundSessionEnvelopeContext,
|
||||
toLocationContext,
|
||||
} from "openclaw/plugin-sdk/channel-inbound";
|
||||
import { recordChannelActivity } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import {
|
||||
ensureConfiguredBindingRouteReady,
|
||||
@@ -14,8 +13,9 @@ import {
|
||||
resolvePinnedMainDmOwnerFromAllowlist,
|
||||
resolveConfiguredBindingRoute,
|
||||
} from "openclaw/plugin-sdk/conversation-runtime";
|
||||
import type { HistoryEntry } from "openclaw/plugin-sdk/reply-history";
|
||||
import { recordChannelActivity } from "openclaw/plugin-sdk/infra-runtime";
|
||||
import { finalizeInboundContext } from "openclaw/plugin-sdk/reply-dispatch-runtime";
|
||||
import type { HistoryEntry } from "openclaw/plugin-sdk/reply-history";
|
||||
import {
|
||||
deriveLastRoutePolicy,
|
||||
resolveAgentIdFromSessionKey,
|
||||
|
||||
@@ -59,7 +59,7 @@ vi.mock("./channel-access-token.js", () => ({
|
||||
resolveLineChannelAccessToken: resolveLineChannelAccessTokenMock,
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/channel-runtime", () => ({
|
||||
vi.mock("openclaw/plugin-sdk/infra-runtime", () => ({
|
||||
recordChannelActivity: recordChannelActivityMock,
|
||||
}));
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { messagingApi } from "@line/bot-sdk";
|
||||
import { recordChannelActivity } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import { loadConfig, type OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { recordChannelActivity } from "openclaw/plugin-sdk/infra-runtime";
|
||||
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { resolveLineAccount } from "./accounts.js";
|
||||
import { resolveLineChannelAccessToken } from "./channel-access-token.js";
|
||||
|
||||
@@ -179,9 +179,9 @@ vi.mock("./daemon.js", async () => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/channel-runtime", async () => {
|
||||
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/channel-runtime")>(
|
||||
"openclaw/plugin-sdk/channel-runtime",
|
||||
vi.mock("openclaw/plugin-sdk/infra-runtime", async () => {
|
||||
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/infra-runtime")>(
|
||||
"openclaw/plugin-sdk/infra-runtime",
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
@@ -197,7 +197,7 @@ export function installSignalToolResultTestHooks() {
|
||||
beforeEach(async () => {
|
||||
const [{ resetInboundDedupe }, { resetSystemEventsForTest }] = await Promise.all([
|
||||
import("openclaw/plugin-sdk/reply-runtime"),
|
||||
import("openclaw/plugin-sdk/channel-runtime"),
|
||||
import("openclaw/plugin-sdk/infra-runtime"),
|
||||
]);
|
||||
resetInboundDedupe();
|
||||
config = {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { waitForTransportReady } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import type { SignalReactionNotificationMode } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { loadConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
@@ -7,6 +6,7 @@ import {
|
||||
resolveDefaultGroupPolicy,
|
||||
warnMissingProviderGroupPolicyFallbackOnce,
|
||||
} from "openclaw/plugin-sdk/config-runtime";
|
||||
import { waitForTransportReady } from "openclaw/plugin-sdk/infra-runtime";
|
||||
import { saveMediaBuffer } from "openclaw/plugin-sdk/media-runtime";
|
||||
import { DEFAULT_GROUP_HISTORY_LIMIT, type HistoryEntry } from "openclaw/plugin-sdk/reply-history";
|
||||
import {
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
resolveMentionGatingWithBypass,
|
||||
} from "openclaw/plugin-sdk/channel-inbound";
|
||||
import { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline";
|
||||
import { enqueueSystemEvent } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import { resolveControlCommandGate } from "openclaw/plugin-sdk/command-auth";
|
||||
import { hasControlCommand } from "openclaw/plugin-sdk/command-auth";
|
||||
import {
|
||||
@@ -29,6 +28,7 @@ import {
|
||||
toInternalMessageReceivedContext,
|
||||
triggerInternalHook,
|
||||
} from "openclaw/plugin-sdk/hook-runtime";
|
||||
import { enqueueSystemEvent } from "openclaw/plugin-sdk/infra-runtime";
|
||||
import { kindFromMime } from "openclaw/plugin-sdk/media-runtime";
|
||||
import {
|
||||
buildPendingHistoryContextFromMap,
|
||||
|
||||
@@ -5,7 +5,7 @@ let registerSlackChannelEvents: typeof import("./channels.js").registerSlackChan
|
||||
let createSlackSystemEventTestHarness: typeof import("./system-event-test-harness.js").createSlackSystemEventTestHarness;
|
||||
|
||||
async function createChannelRuntimeMock(
|
||||
importOriginal: () => Promise<typeof import("openclaw/plugin-sdk/channel-runtime")>,
|
||||
importOriginal: () => Promise<typeof import("openclaw/plugin-sdk/infra-runtime")>,
|
||||
) {
|
||||
const actual = await importOriginal();
|
||||
return {
|
||||
@@ -14,8 +14,8 @@ async function createChannelRuntimeMock(
|
||||
};
|
||||
}
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/channel-runtime", createChannelRuntimeMock);
|
||||
vi.mock("openclaw/plugin-sdk/channel-runtime.js", createChannelRuntimeMock);
|
||||
vi.mock("openclaw/plugin-sdk/infra-runtime", createChannelRuntimeMock);
|
||||
vi.mock("openclaw/plugin-sdk/infra-runtime.js", createChannelRuntimeMock);
|
||||
|
||||
type SlackChannelHandler = (args: {
|
||||
event: Record<string, unknown>;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { SlackEventMiddlewareArgs } from "@slack/bolt";
|
||||
import { resolveChannelConfigWrites } from "openclaw/plugin-sdk/channel-config-writes";
|
||||
import { enqueueSystemEvent } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import { loadConfig, writeConfigFile } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { enqueueSystemEvent } from "openclaw/plugin-sdk/infra-runtime";
|
||||
import { danger, warn } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { migrateSlackChannelConfig } from "../../channel-migration.js";
|
||||
import { resolveSlackChannelLabel } from "../channel-config.js";
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { SlackActionMiddlewareArgs } from "@slack/bolt";
|
||||
import type { Block, KnownBlock } from "@slack/web-api";
|
||||
import { enqueueSystemEvent } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import {
|
||||
buildPluginBindingResolvedText,
|
||||
parsePluginBindingApprovalCustomId,
|
||||
resolvePluginConversationBindingApproval,
|
||||
} from "openclaw/plugin-sdk/conversation-runtime";
|
||||
import { enqueueSystemEvent } from "openclaw/plugin-sdk/infra-runtime";
|
||||
import { dispatchPluginInteractiveHandler } from "openclaw/plugin-sdk/plugin-runtime";
|
||||
import { SLACK_REPLY_BUTTON_ACTION_ID, SLACK_REPLY_SELECT_ACTION_ID } from "../../blocks-render.js";
|
||||
import { authorizeSlackSystemEventSender } from "../auth.js";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { enqueueSystemEvent } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import { enqueueSystemEvent } from "openclaw/plugin-sdk/infra-runtime";
|
||||
import { parseSlackModalPrivateMetadata } from "../../modal-metadata.js";
|
||||
import { authorizeSlackSystemEventSender } from "../auth.js";
|
||||
import type { SlackMonitorContext } from "../context.js";
|
||||
|
||||
@@ -166,7 +166,7 @@ function createContext(overrides?: {
|
||||
|
||||
describe("registerSlackInteractionEvents", () => {
|
||||
beforeAll(async () => {
|
||||
const channelRuntime = await import("openclaw/plugin-sdk/channel-runtime");
|
||||
const channelRuntime = await import("openclaw/plugin-sdk/infra-runtime");
|
||||
const pluginRuntime = await import("openclaw/plugin-sdk/plugin-runtime");
|
||||
const conversationBinding = await import("../../../../../src/plugins/conversation-binding.js");
|
||||
enqueueSystemEventSpy = vi
|
||||
|
||||
@@ -8,14 +8,14 @@ let initSlackHarness: typeof import("./system-event-test-harness.js").createSlac
|
||||
type MemberOverrides = import("./system-event-test-harness.js").SlackSystemEventTestOverrides;
|
||||
|
||||
async function createChannelRuntimeMock(
|
||||
importOriginal: () => Promise<typeof import("openclaw/plugin-sdk/channel-runtime")>,
|
||||
importOriginal: () => Promise<typeof import("openclaw/plugin-sdk/infra-runtime")>,
|
||||
) {
|
||||
const actual = await importOriginal();
|
||||
return { ...actual, enqueueSystemEvent: memberMocks.enqueue };
|
||||
}
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/channel-runtime", createChannelRuntimeMock);
|
||||
vi.mock("openclaw/plugin-sdk/channel-runtime.js", createChannelRuntimeMock);
|
||||
vi.mock("openclaw/plugin-sdk/infra-runtime", createChannelRuntimeMock);
|
||||
vi.mock("openclaw/plugin-sdk/infra-runtime.js", createChannelRuntimeMock);
|
||||
|
||||
type MemberHandler = (args: { event: Record<string, unknown>; body: unknown }) => Promise<void>;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { SlackEventMiddlewareArgs } from "@slack/bolt";
|
||||
import { enqueueSystemEvent } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import { enqueueSystemEvent } from "openclaw/plugin-sdk/infra-runtime";
|
||||
import { danger } from "openclaw/plugin-sdk/runtime-env";
|
||||
import type { SlackMonitorContext } from "../context.js";
|
||||
import type { SlackMemberChannelEvent } from "../types.js";
|
||||
|
||||
@@ -8,7 +8,7 @@ const messageQueueMock = vi.fn();
|
||||
const messageAllowMock = vi.fn();
|
||||
|
||||
async function createChannelRuntimeMock(
|
||||
importOriginal: () => Promise<typeof import("openclaw/plugin-sdk/channel-runtime")>,
|
||||
importOriginal: () => Promise<typeof import("openclaw/plugin-sdk/infra-runtime")>,
|
||||
) {
|
||||
const actual = await importOriginal();
|
||||
return {
|
||||
@@ -17,8 +17,8 @@ async function createChannelRuntimeMock(
|
||||
};
|
||||
}
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/channel-runtime", createChannelRuntimeMock);
|
||||
vi.mock("openclaw/plugin-sdk/channel-runtime.js", createChannelRuntimeMock);
|
||||
vi.mock("openclaw/plugin-sdk/infra-runtime", createChannelRuntimeMock);
|
||||
vi.mock("openclaw/plugin-sdk/infra-runtime.js", createChannelRuntimeMock);
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/conversation-runtime", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/conversation-runtime")>();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { SlackEventMiddlewareArgs } from "@slack/bolt";
|
||||
import { enqueueSystemEvent } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import { enqueueSystemEvent } from "openclaw/plugin-sdk/infra-runtime";
|
||||
import { danger } from "openclaw/plugin-sdk/runtime-env";
|
||||
import type { SlackAppMentionEvent, SlackMessageEvent } from "../../types.js";
|
||||
import { normalizeSlackChannelType } from "../channel-type.js";
|
||||
|
||||
@@ -6,14 +6,14 @@ let buildPinHarness: typeof import("./system-event-test-harness.js").createSlack
|
||||
type PinOverrides = import("./system-event-test-harness.js").SlackSystemEventTestOverrides;
|
||||
|
||||
async function createChannelRuntimeMock(
|
||||
importOriginal: () => Promise<typeof import("openclaw/plugin-sdk/channel-runtime")>,
|
||||
importOriginal: () => Promise<typeof import("openclaw/plugin-sdk/infra-runtime")>,
|
||||
) {
|
||||
const actual = await importOriginal();
|
||||
return { ...actual, enqueueSystemEvent: pinEnqueueMock };
|
||||
}
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/channel-runtime", createChannelRuntimeMock);
|
||||
vi.mock("openclaw/plugin-sdk/channel-runtime.js", createChannelRuntimeMock);
|
||||
vi.mock("openclaw/plugin-sdk/infra-runtime", createChannelRuntimeMock);
|
||||
vi.mock("openclaw/plugin-sdk/infra-runtime.js", createChannelRuntimeMock);
|
||||
|
||||
type PinHandler = (args: { event: Record<string, unknown>; body: unknown }) => Promise<void>;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { SlackEventMiddlewareArgs } from "@slack/bolt";
|
||||
import { enqueueSystemEvent } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import { enqueueSystemEvent } from "openclaw/plugin-sdk/infra-runtime";
|
||||
import { danger } from "openclaw/plugin-sdk/runtime-env";
|
||||
import type { SlackMonitorContext } from "../context.js";
|
||||
import type { SlackPinEvent } from "../types.js";
|
||||
|
||||
@@ -7,7 +7,7 @@ type SlackSystemEventTestOverrides =
|
||||
import("./system-event-test-harness.js").SlackSystemEventTestOverrides;
|
||||
|
||||
async function createChannelRuntimeMock(
|
||||
importOriginal: () => Promise<typeof import("openclaw/plugin-sdk/channel-runtime")>,
|
||||
importOriginal: () => Promise<typeof import("openclaw/plugin-sdk/infra-runtime")>,
|
||||
) {
|
||||
const actual = await importOriginal();
|
||||
return {
|
||||
@@ -16,8 +16,8 @@ async function createChannelRuntimeMock(
|
||||
};
|
||||
}
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/channel-runtime", createChannelRuntimeMock);
|
||||
vi.mock("openclaw/plugin-sdk/channel-runtime.js", createChannelRuntimeMock);
|
||||
vi.mock("openclaw/plugin-sdk/infra-runtime", createChannelRuntimeMock);
|
||||
vi.mock("openclaw/plugin-sdk/infra-runtime.js", createChannelRuntimeMock);
|
||||
|
||||
type ReactionHandler = (args: { event: Record<string, unknown>; body: unknown }) => Promise<void>;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { SlackEventMiddlewareArgs } from "@slack/bolt";
|
||||
import { enqueueSystemEvent } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import { enqueueSystemEvent } from "openclaw/plugin-sdk/infra-runtime";
|
||||
import { danger } from "openclaw/plugin-sdk/runtime-env";
|
||||
import type { SlackMonitorContext } from "../context.js";
|
||||
import type { SlackReactionEvent } from "../types.js";
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
resolveEnvelopeFormatOptions,
|
||||
resolveMentionGatingWithBypass,
|
||||
} from "openclaw/plugin-sdk/channel-inbound";
|
||||
import { enqueueSystemEvent } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import { resolveControlCommandGate } from "openclaw/plugin-sdk/command-auth";
|
||||
import { hasControlCommand } from "openclaw/plugin-sdk/command-auth";
|
||||
import { shouldHandleTextCommands } from "openclaw/plugin-sdk/command-auth";
|
||||
@@ -24,6 +23,7 @@ import {
|
||||
recordInboundSession,
|
||||
resolveConversationLabel,
|
||||
} from "openclaw/plugin-sdk/conversation-runtime";
|
||||
import { enqueueSystemEvent } from "openclaw/plugin-sdk/infra-runtime";
|
||||
import {
|
||||
buildPendingHistoryContextFromMap,
|
||||
recordPendingHistoryEntryIfEnabled,
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
unlinkSync,
|
||||
} from "node:fs";
|
||||
import path from "node:path";
|
||||
import { normalizeChannelId, type ChannelId } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import { normalizeChannelId, type ChannelId } from "openclaw/plugin-sdk/channel-targets";
|
||||
import type {
|
||||
OpenClawConfig,
|
||||
TtsAutoMode,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline";
|
||||
import { enqueueSystemEvent } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import { loadConfig, resolveStorePath } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { loadSessionStore } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { readChannelAllowFromStore } from "openclaw/plugin-sdk/conversation-runtime";
|
||||
import { upsertChannelPairingRequest } from "openclaw/plugin-sdk/conversation-runtime";
|
||||
import { enqueueSystemEvent } from "openclaw/plugin-sdk/infra-runtime";
|
||||
import { buildModelsProviderData } from "openclaw/plugin-sdk/models-provider-runtime";
|
||||
import { dispatchReplyWithBufferedBlockDispatcher } from "openclaw/plugin-sdk/reply-dispatch-runtime";
|
||||
import { listSkillCommandsForAgents } from "openclaw/plugin-sdk/skill-commands-runtime";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export { createStatusReactionController } from "openclaw/plugin-sdk/channel-feedback";
|
||||
export { recordChannelActivity } from "openclaw/plugin-sdk/channel-runtime";
|
||||
export { recordChannelActivity } from "openclaw/plugin-sdk/infra-runtime";
|
||||
export { loadConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
export { ensureConfiguredBindingRouteReady } from "openclaw/plugin-sdk/conversation-runtime";
|
||||
|
||||
@@ -296,8 +296,8 @@ const execApprovalHoisted = vi.hoisted(() => ({
|
||||
}));
|
||||
export const resolveExecApprovalSpy = execApprovalHoisted.resolveExecApprovalSpy;
|
||||
|
||||
vi.doMock("openclaw/plugin-sdk/channel-runtime", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/channel-runtime")>();
|
||||
vi.doMock("openclaw/plugin-sdk/infra-runtime", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/infra-runtime")>();
|
||||
return {
|
||||
...actual,
|
||||
enqueueSystemEvent: systemEventsHoisted.enqueueSystemEventSpy,
|
||||
|
||||
@@ -6,11 +6,11 @@ import type {
|
||||
} from "@grammyjs/types";
|
||||
import { type ApiClientOptions, Bot, HttpError } from "grammy";
|
||||
import * as grammy from "grammy";
|
||||
import { recordChannelActivity } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import { loadConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { isDiagnosticFlagEnabled } from "openclaw/plugin-sdk/diagnostic-runtime";
|
||||
import { formatUncaughtError } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { recordChannelActivity } from "openclaw/plugin-sdk/infra-runtime";
|
||||
import type { MediaKind } from "openclaw/plugin-sdk/media-runtime";
|
||||
import { buildOutboundMediaLoadOptions } from "openclaw/plugin-sdk/media-runtime";
|
||||
import { getImageMetadata } from "openclaw/plugin-sdk/media-runtime";
|
||||
|
||||
@@ -37,7 +37,7 @@ vi.mock("openclaw/plugin-sdk/reply-runtime", async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../../../../src/channels/plugins/whatsapp-heartbeat.js", () => ({
|
||||
vi.mock("../heartbeat-recipients.js", () => ({
|
||||
resolveWhatsAppHeartbeatRecipients: () => [],
|
||||
}));
|
||||
|
||||
@@ -49,8 +49,8 @@ vi.mock("openclaw/plugin-sdk/routing", async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/channel-runtime", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/channel-runtime")>();
|
||||
vi.mock("openclaw/plugin-sdk/infra-runtime", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/infra-runtime")>();
|
||||
return {
|
||||
...actual,
|
||||
resolveHeartbeatVisibility: () => state.visibility,
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import { appendCronStyleCurrentTimeLine } from "openclaw/plugin-sdk/agent-runtime";
|
||||
import {
|
||||
emitHeartbeatEvent,
|
||||
resolveHeartbeatVisibility,
|
||||
resolveIndicatorType,
|
||||
} from "openclaw/plugin-sdk/channel-runtime";
|
||||
import { canonicalizeMainSessionAlias, loadConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import {
|
||||
loadSessionStore,
|
||||
@@ -11,6 +6,11 @@ import {
|
||||
resolveStorePath,
|
||||
updateSessionStore,
|
||||
} from "openclaw/plugin-sdk/config-runtime";
|
||||
import {
|
||||
emitHeartbeatEvent,
|
||||
resolveHeartbeatVisibility,
|
||||
resolveIndicatorType,
|
||||
} from "openclaw/plugin-sdk/infra-runtime";
|
||||
import {
|
||||
hasOutboundReplyContent,
|
||||
resolveSendableOutboundReplyParts,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { resolveInboundDebounceMs } from "openclaw/plugin-sdk/channel-inbound";
|
||||
import { enqueueSystemEvent } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import { formatCliCommand } from "openclaw/plugin-sdk/cli-runtime";
|
||||
import { waitForever } from "openclaw/plugin-sdk/cli-runtime";
|
||||
import { hasControlCommand } from "openclaw/plugin-sdk/command-detection";
|
||||
import { loadConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { enqueueSystemEvent } from "openclaw/plugin-sdk/infra-runtime";
|
||||
import { DEFAULT_GROUP_HISTORY_LIMIT } from "openclaw/plugin-sdk/reply-history";
|
||||
import { getReplyFromConfig } from "openclaw/plugin-sdk/reply-runtime";
|
||||
import { resolveAgentRoute } from "openclaw/plugin-sdk/routing";
|
||||
|
||||
15
extensions/whatsapp/src/group-intro.ts
Normal file
15
extensions/whatsapp/src/group-intro.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export const WHATSAPP_GROUP_INTRO_HINT =
|
||||
"WhatsApp IDs: SenderId is the participant JID (group participant id).";
|
||||
|
||||
export function resolveWhatsAppGroupIntroHint(): string {
|
||||
return WHATSAPP_GROUP_INTRO_HINT;
|
||||
}
|
||||
|
||||
export function resolveWhatsAppMentionStripRegexes(ctx: { To?: string | null }): RegExp[] {
|
||||
const selfE164 = (ctx.To ?? "").replace(/^whatsapp:/i, "");
|
||||
if (!selfE164) {
|
||||
return [];
|
||||
}
|
||||
const escaped = selfE164.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
return [new RegExp(escaped, "g"), new RegExp(`@${escaped}`, "g")];
|
||||
}
|
||||
@@ -43,15 +43,31 @@ describe("resolveWhatsAppHeartbeatRecipients", () => {
|
||||
vi.resetModules();
|
||||
loadSessionStoreMock.mockReset();
|
||||
readChannelAllowFromStoreSyncMock.mockReset();
|
||||
vi.doMock("../../../src/config/sessions/store-summary.js", () => ({
|
||||
loadSessionStoreSummary: loadSessionStoreMock,
|
||||
}));
|
||||
vi.doMock("../../../src/config/sessions/paths.js", () => ({
|
||||
resolveStorePath: vi.fn(() => "/tmp/test-sessions.json"),
|
||||
}));
|
||||
vi.doMock("../../../src/pairing/pairing-store.js", () => ({
|
||||
readChannelAllowFromStoreSync: readChannelAllowFromStoreSyncMock,
|
||||
}));
|
||||
vi.doMock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/config-runtime")>();
|
||||
return {
|
||||
...actual,
|
||||
loadSessionStore: loadSessionStoreMock,
|
||||
resolveStorePath: vi.fn(() => "/tmp/test-sessions.json"),
|
||||
};
|
||||
});
|
||||
vi.doMock("openclaw/plugin-sdk/channel-pairing", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/channel-pairing")>();
|
||||
return {
|
||||
...actual,
|
||||
readChannelAllowFromStoreSync: readChannelAllowFromStoreSyncMock,
|
||||
};
|
||||
});
|
||||
vi.doMock("openclaw/plugin-sdk/channel-targets", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/channel-targets")>();
|
||||
return {
|
||||
...actual,
|
||||
normalizeChannelId: (value?: string | null) => {
|
||||
const trimmed = value?.trim().toLowerCase();
|
||||
return trimmed ? (trimmed as "whatsapp") : null;
|
||||
},
|
||||
};
|
||||
});
|
||||
({ resolveWhatsAppHeartbeatRecipients } = await import("./runtime-api.js"));
|
||||
setAllowFromStore([]);
|
||||
});
|
||||
|
||||
100
extensions/whatsapp/src/heartbeat-recipients.ts
Normal file
100
extensions/whatsapp/src/heartbeat-recipients.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
|
||||
import { normalizeE164 } from "openclaw/plugin-sdk/account-resolution";
|
||||
import { readChannelAllowFromStoreSync } from "openclaw/plugin-sdk/channel-pairing";
|
||||
import { normalizeChannelId } from "openclaw/plugin-sdk/channel-targets";
|
||||
import {
|
||||
loadSessionStore,
|
||||
resolveStorePath,
|
||||
type OpenClawConfig,
|
||||
} from "openclaw/plugin-sdk/config-runtime";
|
||||
|
||||
type HeartbeatRecipientsResult = { recipients: string[]; source: string };
|
||||
type HeartbeatRecipientsOpts = { to?: string; all?: boolean };
|
||||
|
||||
function getSessionRecipients(cfg: OpenClawConfig) {
|
||||
const sessionCfg = cfg.session;
|
||||
const scope = sessionCfg?.scope ?? "per-sender";
|
||||
if (scope === "global") {
|
||||
return [];
|
||||
}
|
||||
const storePath = resolveStorePath(cfg.session?.store);
|
||||
const store = loadSessionStore(storePath);
|
||||
const isGroupKey = (key: string) =>
|
||||
key.includes(":group:") || key.includes(":channel:") || key.includes("@g.us");
|
||||
const isCronKey = (key: string) => key.startsWith("cron:");
|
||||
|
||||
const recipients = Object.entries(store)
|
||||
.filter(([key]) => key !== "global" && key !== "unknown")
|
||||
.filter(([key]) => !isGroupKey(key) && !isCronKey(key))
|
||||
.map(([_, entry]) => ({
|
||||
to:
|
||||
normalizeChannelId(entry?.lastChannel) === "whatsapp" && entry?.lastTo
|
||||
? normalizeE164(entry.lastTo)
|
||||
: "",
|
||||
updatedAt: entry?.updatedAt ?? 0,
|
||||
}))
|
||||
.filter(({ to }) => to.length > 1)
|
||||
.toSorted((a, b) => b.updatedAt - a.updatedAt);
|
||||
|
||||
const seen = new Set<string>();
|
||||
return recipients.filter((recipient) => {
|
||||
if (seen.has(recipient.to)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(recipient.to);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveWhatsAppHeartbeatRecipients(
|
||||
cfg: OpenClawConfig,
|
||||
opts: HeartbeatRecipientsOpts = {},
|
||||
): HeartbeatRecipientsResult {
|
||||
if (opts.to) {
|
||||
return { recipients: [normalizeE164(opts.to)], source: "flag" };
|
||||
}
|
||||
|
||||
const sessionRecipients = getSessionRecipients(cfg);
|
||||
const configuredAllowFrom =
|
||||
Array.isArray(cfg.channels?.whatsapp?.allowFrom) && cfg.channels.whatsapp.allowFrom.length > 0
|
||||
? cfg.channels.whatsapp.allowFrom.filter((value) => value !== "*").map(normalizeE164)
|
||||
: [];
|
||||
const storeAllowFrom = readChannelAllowFromStoreSync(
|
||||
"whatsapp",
|
||||
process.env,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
).map(normalizeE164);
|
||||
|
||||
const unique = (list: string[]) => [...new Set(list.filter(Boolean))];
|
||||
const allowFrom = unique([...configuredAllowFrom, ...storeAllowFrom]);
|
||||
|
||||
if (opts.all) {
|
||||
return {
|
||||
recipients: unique([...sessionRecipients.map((entry) => entry.to), ...allowFrom]),
|
||||
source: "all",
|
||||
};
|
||||
}
|
||||
|
||||
if (allowFrom.length > 0) {
|
||||
const allowSet = new Set(allowFrom);
|
||||
const authorizedSessionRecipients = sessionRecipients
|
||||
.map((entry) => entry.to)
|
||||
.filter((recipient) => allowSet.has(recipient));
|
||||
if (authorizedSessionRecipients.length === 1) {
|
||||
return { recipients: [authorizedSessionRecipients[0]], source: "session-single" };
|
||||
}
|
||||
if (authorizedSessionRecipients.length > 1) {
|
||||
return { recipients: authorizedSessionRecipients, source: "session-ambiguous" };
|
||||
}
|
||||
return { recipients: allowFrom, source: "allowFrom" };
|
||||
}
|
||||
|
||||
if (sessionRecipients.length === 1) {
|
||||
return { recipients: [sessionRecipients[0].to], source: "session-single" };
|
||||
}
|
||||
if (sessionRecipients.length > 1) {
|
||||
return { recipients: sessionRecipients.map((entry) => entry.to), source: "session-ambiguous" };
|
||||
}
|
||||
|
||||
return { recipients: allowFrom, source: "allowFrom" };
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { AnyMessageContent, proto, WAMessage } from "@whiskeysockets/baileys";
|
||||
import { DisconnectReason, isJidGroup } from "@whiskeysockets/baileys";
|
||||
import { createInboundDebouncer, formatLocationText } from "openclaw/plugin-sdk/channel-inbound";
|
||||
import { recordChannelActivity } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import { recordChannelActivity } from "openclaw/plugin-sdk/infra-runtime";
|
||||
import { saveMediaBuffer } from "openclaw/plugin-sdk/media-runtime";
|
||||
import { logVerbose, shouldLogVerbose } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
|
||||
|
||||
@@ -3,8 +3,8 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
const recordChannelActivity = vi.hoisted(() => vi.fn());
|
||||
let createWebSendApi: typeof import("./send-api.js").createWebSendApi;
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/channel-runtime", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/channel-runtime")>();
|
||||
vi.mock("openclaw/plugin-sdk/infra-runtime", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/infra-runtime")>();
|
||||
return {
|
||||
...actual,
|
||||
recordChannelActivity: (...args: unknown[]) => recordChannelActivity(...args),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AnyMessageContent, WAPresence } from "@whiskeysockets/baileys";
|
||||
import { recordChannelActivity } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import { recordChannelActivity } from "openclaw/plugin-sdk/infra-runtime";
|
||||
import { toWhatsappJid } from "openclaw/plugin-sdk/text-runtime";
|
||||
import type { ActiveWebSendOptions } from "../active-listener.js";
|
||||
|
||||
|
||||
@@ -67,3 +67,32 @@ export function normalizeWhatsAppTarget(value: string): string | null {
|
||||
const normalized = normalizeE164(candidate);
|
||||
return normalized.length > 1 ? normalized : null;
|
||||
}
|
||||
|
||||
export function normalizeWhatsAppMessagingTarget(raw: string): string | undefined {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
return normalizeWhatsAppTarget(trimmed) ?? undefined;
|
||||
}
|
||||
|
||||
export function normalizeWhatsAppAllowFromEntries(allowFrom: Array<string | number>): string[] {
|
||||
return allowFrom
|
||||
.map((entry) => String(entry).trim())
|
||||
.filter((entry): entry is string => Boolean(entry))
|
||||
.map((entry) => (entry === "*" ? entry : normalizeWhatsAppTarget(entry)))
|
||||
.filter((entry): entry is string => Boolean(entry));
|
||||
}
|
||||
|
||||
export function looksLikeWhatsAppTargetId(raw: string): boolean {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
/^whatsapp:/i.test(trimmed) ||
|
||||
isWhatsAppGroupJid(trimmed) ||
|
||||
isWhatsAppUserTarget(trimmed) ||
|
||||
normalizeWhatsAppTarget(trimmed) !== null
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ export {
|
||||
looksLikeWhatsAppTargetId,
|
||||
normalizeWhatsAppAllowFromEntries,
|
||||
normalizeWhatsAppMessagingTarget,
|
||||
} from "./runtime-api.js";
|
||||
export {
|
||||
isWhatsAppGroupJid,
|
||||
isWhatsAppUserTarget,
|
||||
normalizeWhatsAppTarget,
|
||||
|
||||
119
extensions/whatsapp/src/outbound-base.ts
Normal file
119
extensions/whatsapp/src/outbound-base.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import {
|
||||
createAttachedChannelResultAdapter,
|
||||
type ChannelOutboundAdapter,
|
||||
} from "openclaw/plugin-sdk/channel-send-result";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { resolveOutboundSendDep } from "openclaw/plugin-sdk/infra-runtime";
|
||||
|
||||
type WhatsAppChunker = NonNullable<ChannelOutboundAdapter["chunker"]>;
|
||||
type WhatsAppSendTextOptions = {
|
||||
verbose: boolean;
|
||||
cfg?: OpenClawConfig;
|
||||
mediaUrl?: string;
|
||||
mediaAccess?: {
|
||||
localRoots?: readonly string[];
|
||||
readFile?: (filePath: string) => Promise<Buffer>;
|
||||
};
|
||||
mediaLocalRoots?: readonly string[];
|
||||
mediaReadFile?: (filePath: string) => Promise<Buffer>;
|
||||
gifPlayback?: boolean;
|
||||
accountId?: string;
|
||||
};
|
||||
type WhatsAppSendMessage = (
|
||||
to: string,
|
||||
body: string,
|
||||
options: WhatsAppSendTextOptions,
|
||||
) => Promise<{ messageId: string; toJid: string }>;
|
||||
type WhatsAppSendPoll = (
|
||||
to: string,
|
||||
poll: Parameters<NonNullable<ChannelOutboundAdapter["sendPoll"]>>[0]["poll"],
|
||||
options: { verbose: boolean; accountId?: string; cfg?: OpenClawConfig },
|
||||
) => Promise<{ messageId: string; toJid: string }>;
|
||||
|
||||
type CreateWhatsAppOutboundBaseParams = {
|
||||
chunker: WhatsAppChunker;
|
||||
sendMessageWhatsApp: WhatsAppSendMessage;
|
||||
sendPollWhatsApp: WhatsAppSendPoll;
|
||||
shouldLogVerbose: () => boolean;
|
||||
resolveTarget: ChannelOutboundAdapter["resolveTarget"];
|
||||
normalizeText?: (text: string | undefined) => string;
|
||||
skipEmptyText?: boolean;
|
||||
};
|
||||
|
||||
export function createWhatsAppOutboundBase({
|
||||
chunker,
|
||||
sendMessageWhatsApp,
|
||||
sendPollWhatsApp,
|
||||
shouldLogVerbose,
|
||||
resolveTarget,
|
||||
normalizeText = (text) => text ?? "",
|
||||
skipEmptyText = false,
|
||||
}: CreateWhatsAppOutboundBaseParams): Pick<
|
||||
ChannelOutboundAdapter,
|
||||
| "deliveryMode"
|
||||
| "chunker"
|
||||
| "chunkerMode"
|
||||
| "textChunkLimit"
|
||||
| "pollMaxOptions"
|
||||
| "resolveTarget"
|
||||
| "sendText"
|
||||
| "sendMedia"
|
||||
| "sendPoll"
|
||||
> {
|
||||
return {
|
||||
deliveryMode: "gateway",
|
||||
chunker,
|
||||
chunkerMode: "text",
|
||||
textChunkLimit: 4000,
|
||||
pollMaxOptions: 12,
|
||||
resolveTarget,
|
||||
...createAttachedChannelResultAdapter({
|
||||
channel: "whatsapp",
|
||||
sendText: async ({ cfg, to, text, accountId, deps, gifPlayback }) => {
|
||||
const normalizedText = normalizeText(text);
|
||||
if (skipEmptyText && !normalizedText) {
|
||||
return { messageId: "" };
|
||||
}
|
||||
const send =
|
||||
resolveOutboundSendDep<WhatsAppSendMessage>(deps, "whatsapp") ?? sendMessageWhatsApp;
|
||||
return await send(to, normalizedText, {
|
||||
verbose: false,
|
||||
cfg,
|
||||
accountId: accountId ?? undefined,
|
||||
gifPlayback,
|
||||
});
|
||||
},
|
||||
sendMedia: async ({
|
||||
cfg,
|
||||
to,
|
||||
text,
|
||||
mediaUrl,
|
||||
mediaAccess,
|
||||
mediaLocalRoots,
|
||||
mediaReadFile,
|
||||
accountId,
|
||||
deps,
|
||||
gifPlayback,
|
||||
}) => {
|
||||
const send =
|
||||
resolveOutboundSendDep<WhatsAppSendMessage>(deps, "whatsapp") ?? sendMessageWhatsApp;
|
||||
return await send(to, normalizeText(text), {
|
||||
verbose: false,
|
||||
cfg,
|
||||
mediaUrl,
|
||||
mediaAccess,
|
||||
mediaLocalRoots,
|
||||
mediaReadFile,
|
||||
accountId: accountId ?? undefined,
|
||||
gifPlayback,
|
||||
});
|
||||
},
|
||||
sendPoll: async ({ cfg, to, poll, accountId }) =>
|
||||
await sendPollWhatsApp(to, poll, {
|
||||
verbose: shouldLogVerbose(),
|
||||
accountId: accountId ?? undefined,
|
||||
cfg,
|
||||
}),
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -19,24 +19,24 @@ export { normalizeE164 } from "openclaw/plugin-sdk/account-resolution";
|
||||
export type { DmPolicy, GroupPolicy } from "openclaw/plugin-sdk/config-runtime";
|
||||
import type { OpenClawConfig as RuntimeOpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
|
||||
export {
|
||||
type ChannelMessageActionName,
|
||||
createWhatsAppOutboundBase,
|
||||
looksLikeWhatsAppTargetId,
|
||||
normalizeWhatsAppAllowFromEntries,
|
||||
normalizeWhatsAppMessagingTarget,
|
||||
resolveWhatsAppGroupIntroHint,
|
||||
resolveWhatsAppHeartbeatRecipients,
|
||||
resolveWhatsAppMentionStripRegexes,
|
||||
} from "openclaw/plugin-sdk/channel-runtime";
|
||||
export { type ChannelMessageActionName } from "openclaw/plugin-sdk/channel-contract";
|
||||
import { loadWebMedia } from "openclaw/plugin-sdk/web-media";
|
||||
export {
|
||||
resolveWhatsAppGroupRequireMention,
|
||||
resolveWhatsAppGroupToolPolicy,
|
||||
} from "./group-policy.js";
|
||||
export {
|
||||
resolveWhatsAppGroupIntroHint,
|
||||
resolveWhatsAppMentionStripRegexes,
|
||||
} from "./group-intro.js";
|
||||
export { resolveWhatsAppHeartbeatRecipients } from "./heartbeat-recipients.js";
|
||||
export { createWhatsAppOutboundBase } from "./outbound-base.js";
|
||||
export {
|
||||
isWhatsAppGroupJid,
|
||||
isWhatsAppUserTarget,
|
||||
looksLikeWhatsAppTargetId,
|
||||
normalizeWhatsAppAllowFromEntries,
|
||||
normalizeWhatsAppMessagingTarget,
|
||||
normalizeWhatsAppTarget,
|
||||
} from "./normalize-target.js";
|
||||
export { resolveWhatsAppOutboundTarget } from "./resolve-outbound-target.js";
|
||||
|
||||
@@ -141,7 +141,7 @@ export function installReplyRuntimeMocks(mocks: ReplyRuntimeMocks) {
|
||||
listSkillCommandsForWorkspace: () => [],
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/runtime/runtime-whatsapp-boundary.js", () => ({
|
||||
vi.mock("../plugins/runtime/runtime-web-channel-boundary.js", () => ({
|
||||
webAuthExists: mocks.webAuthExists,
|
||||
getWebAuthAgeMs: mocks.getWebAuthAgeMs,
|
||||
readWebSelfId: mocks.readWebSelfId,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Barrel exports for the web channel pieces. Splitting the original 900+ line
|
||||
// module keeps responsibilities small and testable.
|
||||
import { resolveWaWebAuthDir } from "./plugins/runtime/runtime-whatsapp-boundary.js";
|
||||
import { resolveWaWebAuthDir } from "./plugins/runtime/runtime-web-channel-boundary.js";
|
||||
|
||||
export { HEARTBEAT_PROMPT } from "./auto-reply/heartbeat.js";
|
||||
export { HEARTBEAT_TOKEN } from "./auto-reply/tokens.js";
|
||||
@@ -23,7 +23,7 @@ export {
|
||||
sendReactionWhatsApp,
|
||||
waitForWaConnection,
|
||||
webAuthExists,
|
||||
} from "./plugins/runtime/runtime-whatsapp-boundary.js";
|
||||
} from "./plugins/runtime/runtime-web-channel-boundary.js";
|
||||
|
||||
// Keep the historic constant surface available, but resolve it through the
|
||||
// plugin boundary only when a caller actually coerces the value to string.
|
||||
|
||||
@@ -29,14 +29,14 @@ describe("normalizeChatType", () => {
|
||||
|
||||
describe("WA_WEB_AUTH_DIR", () => {
|
||||
afterEach(() => {
|
||||
vi.doUnmock("../plugins/runtime/runtime-whatsapp-boundary.js");
|
||||
vi.doUnmock("../plugins/runtime/runtime-web-channel-boundary.js");
|
||||
});
|
||||
|
||||
it("resolves lazily and caches across the legacy and channels/web entrypoints", async () => {
|
||||
const resolveWaWebAuthDir = vi.fn(() => "/tmp/openclaw-whatsapp-auth");
|
||||
|
||||
vi.resetModules();
|
||||
vi.doMock("../plugins/runtime/runtime-whatsapp-boundary.js", () => ({
|
||||
vi.doMock("../plugins/runtime/runtime-web-channel-boundary.js", () => ({
|
||||
createWaSocket: vi.fn(),
|
||||
extractMediaPlaceholder: vi.fn(),
|
||||
extractText: vi.fn(),
|
||||
|
||||
@@ -21,9 +21,11 @@ const sendFns = vi.hoisted(() => ({
|
||||
|
||||
const whatsappBoundaryLoads = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("../plugins/runtime/runtime-whatsapp-boundary.js", async (importOriginal) => {
|
||||
vi.mock("../plugins/runtime/runtime-web-channel-boundary.js", async (importOriginal) => {
|
||||
whatsappBoundaryLoads();
|
||||
return await importOriginal<typeof import("../plugins/runtime/runtime-whatsapp-boundary.js")>();
|
||||
return await importOriginal<
|
||||
typeof import("../plugins/runtime/runtime-web-channel-boundary.js")
|
||||
>();
|
||||
});
|
||||
|
||||
vi.mock("./send-runtime/whatsapp.js", () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { sendMessageWhatsApp as sendMessageWhatsAppImpl } from "../../plugins/runtime/runtime-whatsapp-boundary.js";
|
||||
import { sendMessageWhatsApp as sendMessageWhatsAppImpl } from "../../plugins/runtime/runtime-web-channel-boundary.js";
|
||||
|
||||
type RuntimeSend = {
|
||||
sendMessage: typeof import("../../plugins/runtime/runtime-whatsapp-boundary.js").sendMessageWhatsApp;
|
||||
sendMessage: typeof import("../../plugins/runtime/runtime-web-channel-boundary.js").sendMessageWhatsApp;
|
||||
};
|
||||
|
||||
export const runtimeSend = {
|
||||
|
||||
@@ -41,7 +41,7 @@ async function loadFreshHealthModulesForTest() {
|
||||
recordSessionMetaFromInbound: vi.fn().mockResolvedValue(undefined),
|
||||
updateLastRoute: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
vi.doMock("../plugins/runtime/runtime-whatsapp-boundary.js", () => ({
|
||||
vi.doMock("../plugins/runtime/runtime-web-channel-boundary.js", () => ({
|
||||
webAuthExists: vi.fn(async () => true),
|
||||
getWebAuthAgeMs: vi.fn(() => 1234),
|
||||
readWebSelfId: vi.fn(() => ({ e164: null, jid: null })),
|
||||
|
||||
@@ -383,7 +383,7 @@ vi.mock("../channels/plugins/index.js", () => ({
|
||||
},
|
||||
] as unknown,
|
||||
}));
|
||||
vi.mock("../plugins/runtime/runtime-whatsapp-boundary.js", () => ({
|
||||
vi.mock("../plugins/runtime/runtime-web-channel-boundary.js", () => ({
|
||||
webAuthExists: mocks.webAuthExists,
|
||||
getWebAuthAgeMs: mocks.getWebAuthAgeMs,
|
||||
readWebSelfId: mocks.readWebSelfId,
|
||||
|
||||
@@ -821,11 +821,11 @@ vi.mock("../plugins/loader.js", async () => {
|
||||
loadOpenClawPlugins: () => pluginRegistryState.registry,
|
||||
};
|
||||
});
|
||||
vi.mock("../plugins/runtime/runtime-whatsapp-boundary.js", () => ({
|
||||
vi.mock("../plugins/runtime/runtime-web-channel-boundary.js", () => ({
|
||||
sendMessageWhatsApp: (...args: unknown[]) =>
|
||||
(hoisted.sendWhatsAppMock as (...args: unknown[]) => unknown)(...args),
|
||||
}));
|
||||
vi.mock("/src/plugins/runtime/runtime-whatsapp-boundary.js", () => ({
|
||||
vi.mock("/src/plugins/runtime/runtime-web-channel-boundary.js", () => ({
|
||||
sendMessageWhatsApp: (...args: unknown[]) =>
|
||||
(hoisted.sendWhatsAppMock as (...args: unknown[]) => unknown)(...args),
|
||||
}));
|
||||
|
||||
@@ -1,14 +1,34 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { telegramOutbound, whatsappOutbound } from "../../../test/channel-outbounds.js";
|
||||
import {
|
||||
isWhatsAppGroupJid,
|
||||
normalizeWhatsAppTarget,
|
||||
} from "../../channels/plugins/normalize/whatsapp.js";
|
||||
import type { ChannelOutboundAdapter } from "../../channels/plugins/types.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { setActivePluginRegistry } from "../../plugins/runtime.js";
|
||||
import { createOutboundTestPlugin, createTestRegistry } from "../../test-utils/channel-plugins.js";
|
||||
import { resolveOutboundTarget } from "./targets.js";
|
||||
|
||||
const createOutboundStub = (channel: "telegram" | "whatsapp"): ChannelOutboundAdapter => ({
|
||||
deliveryMode: channel === "whatsapp" ? "gateway" : "direct",
|
||||
resolveTarget:
|
||||
channel === "whatsapp"
|
||||
? ({ to }) => {
|
||||
const normalized = to ? normalizeWhatsAppTarget(to) : null;
|
||||
return normalized
|
||||
? { ok: true, to: normalized }
|
||||
: { ok: false, error: new Error("WhatsApp target required") };
|
||||
}
|
||||
: ({ to }) =>
|
||||
typeof to === "string" && to.trim()
|
||||
? { ok: true, to: to.trim() }
|
||||
: { ok: false, error: new Error("Telegram target required") },
|
||||
sendText: async () => ({ channel, messageId: `${channel}-msg` }),
|
||||
});
|
||||
|
||||
const telegramOutbound = createOutboundStub("telegram");
|
||||
const whatsappOutbound = createOutboundStub("whatsapp");
|
||||
|
||||
function parseTelegramTargetForTest(raw: string): {
|
||||
chatId: string;
|
||||
messageThreadId?: number;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { telegramOutbound, whatsappOutbound } from "../../../test/channel-outbounds.js";
|
||||
import {
|
||||
isWhatsAppGroupJid,
|
||||
normalizeWhatsAppTarget,
|
||||
@@ -21,6 +20,26 @@ import {
|
||||
} from "./targets.shared-test.js";
|
||||
import { telegramMessagingForTest } from "./targets.test-helpers.js";
|
||||
|
||||
const createOutboundStub = (channel: "telegram" | "whatsapp"): ChannelOutboundAdapter => ({
|
||||
deliveryMode: channel === "whatsapp" ? "gateway" : "direct",
|
||||
resolveTarget:
|
||||
channel === "whatsapp"
|
||||
? ({ to }) => {
|
||||
const normalized = to ? normalizeWhatsAppTarget(to) : null;
|
||||
return normalized
|
||||
? { ok: true, to: normalized }
|
||||
: { ok: false, error: new Error("WhatsApp target required") };
|
||||
}
|
||||
: ({ to }) =>
|
||||
typeof to === "string" && to.trim()
|
||||
? { ok: true, to: to.trim() }
|
||||
: { ok: false, error: new Error("Telegram target required") },
|
||||
sendText: async () => ({ channel, messageId: `${channel}-msg` }),
|
||||
});
|
||||
|
||||
const telegramOutbound = createOutboundStub("telegram");
|
||||
const whatsappOutbound = createOutboundStub("whatsapp");
|
||||
|
||||
runResolveOutboundTargetCoreTests();
|
||||
|
||||
const whatsappMessaging = {
|
||||
|
||||
@@ -10,7 +10,7 @@ const lazyRuntimeSpecifiers = [
|
||||
"./cli/prompt.js",
|
||||
"./infra/binaries.js",
|
||||
"./process/exec.js",
|
||||
"./plugins/runtime/runtime-whatsapp-boundary.js",
|
||||
"./plugins/runtime/runtime-web-channel-boundary.js",
|
||||
] as const;
|
||||
|
||||
function readLibraryModuleImports() {
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
handlePortError,
|
||||
PortInUseError,
|
||||
} from "./infra/ports.js";
|
||||
import type { monitorWebChannel as monitorWebChannelRuntime } from "./plugins/runtime/runtime-whatsapp-boundary.js";
|
||||
import type { monitorWebChannel as monitorWebChannelRuntime } from "./plugins/runtime/runtime-web-channel-boundary.js";
|
||||
import type {
|
||||
runCommandWithTimeout as runCommandWithTimeoutRuntime,
|
||||
runExec as runExecRuntime,
|
||||
@@ -33,7 +33,7 @@ let promptRuntimePromise: Promise<typeof import("./cli/prompt.js")> | null = nul
|
||||
let binariesRuntimePromise: Promise<typeof import("./infra/binaries.js")> | null = null;
|
||||
let execRuntimePromise: Promise<typeof import("./process/exec.js")> | null = null;
|
||||
let whatsappRuntimePromise: Promise<
|
||||
typeof import("./plugins/runtime/runtime-whatsapp-boundary.js")
|
||||
typeof import("./plugins/runtime/runtime-web-channel-boundary.js")
|
||||
> | null = null;
|
||||
|
||||
function loadReplyRuntime() {
|
||||
@@ -57,7 +57,7 @@ function loadExecRuntime() {
|
||||
}
|
||||
|
||||
function loadWhatsAppRuntime() {
|
||||
whatsappRuntimePromise ??= import("./plugins/runtime/runtime-whatsapp-boundary.js");
|
||||
whatsappRuntimePromise ??= import("./plugins/runtime/runtime-web-channel-boundary.js");
|
||||
return whatsappRuntimePromise;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,3 +18,5 @@ export type {
|
||||
ChannelThreadingContext,
|
||||
ChannelThreadingToolContext,
|
||||
} from "../channels/plugins/types.js";
|
||||
|
||||
export type { ChannelDirectoryAdapter } from "../channels/plugins/types.adapters.js";
|
||||
|
||||
@@ -4,6 +4,7 @@ export {
|
||||
createPairingPrefixStripper,
|
||||
createTextPairingAdapter,
|
||||
} from "../channels/plugins/pairing-adapters.js";
|
||||
export { readChannelAllowFromStoreSync } from "../pairing/pairing-store.js";
|
||||
import { issuePairingChallenge } from "../pairing/pairing-challenge.js";
|
||||
import type { PluginRuntime } from "../plugins/runtime/types.js";
|
||||
import { createScopedPairingAccess } from "./pairing-access.js";
|
||||
|
||||
@@ -6,15 +6,7 @@ export * from "../channels/reply-prefix.js";
|
||||
export * from "../channels/typing.js";
|
||||
export type * from "../channels/plugins/types.js";
|
||||
export { normalizeChannelId } from "../channels/plugins/registry.js";
|
||||
export * from "../channels/plugins/normalize/signal.js";
|
||||
export * from "../channels/plugins/normalize/whatsapp.js";
|
||||
export * from "../channels/plugins/outbound/interactive.js";
|
||||
export * from "../channels/plugins/whatsapp-heartbeat.js";
|
||||
export {
|
||||
createWhatsAppOutboundBase,
|
||||
resolveWhatsAppGroupIntroHint,
|
||||
resolveWhatsAppMentionStripRegexes,
|
||||
} from "../channels/plugins/whatsapp-shared.js";
|
||||
export * from "../polls.js";
|
||||
export { enqueueSystemEvent, resetSystemEventsForTest } from "../infra/system-events.js";
|
||||
export { recordChannelActivity } from "../infra/channel-activity.js";
|
||||
|
||||
@@ -37,6 +37,8 @@ export {
|
||||
type ParsedChatTarget,
|
||||
type ServicePrefix,
|
||||
} from "../channels/plugins/chat-target-prefixes.js";
|
||||
export type { ChannelId } from "../channels/plugins/types.js";
|
||||
export { normalizeChannelId } from "../channels/plugins/registry.js";
|
||||
export {
|
||||
buildUnresolvedTargetResults,
|
||||
resolveTargetsWithOptionalToken,
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { handleWhatsAppAction } from "../plugins/runtime/runtime-whatsapp-boundary.js";
|
||||
@@ -1,4 +0,0 @@
|
||||
export {
|
||||
startWebLoginWithQr,
|
||||
waitForWebLogin,
|
||||
} from "../plugins/runtime/runtime-whatsapp-boundary.js";
|
||||
@@ -151,6 +151,19 @@ describe("plugin-sdk subpath exports", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps removed bundled-channel prefixes out of the public sdk list", () => {
|
||||
const bannedPrefixes = ["discord", "signal", "slack", "telegram", "whatsapp"];
|
||||
const banned = pluginSdkSubpaths.filter((subpath) =>
|
||||
bannedPrefixes.some(
|
||||
(prefix) =>
|
||||
subpath === prefix ||
|
||||
subpath.startsWith(`${prefix}-`) ||
|
||||
subpath.startsWith(`${prefix}.`),
|
||||
),
|
||||
);
|
||||
expect(banned).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps helper subpaths aligned", () => {
|
||||
expectSourceMentions("core", [
|
||||
"emptyPluginConfigSchema",
|
||||
@@ -468,8 +481,10 @@ describe("plugin-sdk subpath exports", () => {
|
||||
"applyChannelMatchMeta",
|
||||
"buildChannelKeyCandidates",
|
||||
"buildMessagingTarget",
|
||||
"ChannelId",
|
||||
"createAllowedChatSenderMatcher",
|
||||
"ensureTargetId",
|
||||
"normalizeChannelId",
|
||||
"parseChatAllowTargetPrefixes",
|
||||
"parseMentionPrefixOrAtUserTarget",
|
||||
"parseChatTargetPrefixesOrThrow",
|
||||
@@ -775,6 +790,7 @@ describe("plugin-sdk subpath exports", () => {
|
||||
"createChannelPairingChallengeIssuer",
|
||||
"createLoggedPairingApprovalNotifier",
|
||||
"createPairingPrefixStripper",
|
||||
"readChannelAllowFromStoreSync",
|
||||
"createTextPairingAdapter",
|
||||
]);
|
||||
expect("createScopedPairingAccess" in channelPairingSdk).toBe(false);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { createJiti } from "jiti";
|
||||
type WhatsAppHeavyRuntimeModule = typeof import("@openclaw/whatsapp/runtime-api.js");
|
||||
type WhatsAppLightRuntimeModule = typeof import("@openclaw/whatsapp/light-runtime-api.js");
|
||||
import { resolveWhatsAppHeartbeatRecipients } from "../../channels/plugins/whatsapp-heartbeat.js";
|
||||
import {
|
||||
getDefaultLocalRoots as getDefaultLocalRootsImpl,
|
||||
loadWebMedia as loadWebMediaImpl,
|
||||
@@ -281,7 +280,7 @@ export function getDefaultLocalRoots(
|
||||
}
|
||||
|
||||
export function resolveHeartbeatRecipients(
|
||||
...args: Parameters<typeof resolveWhatsAppHeartbeatRecipients>
|
||||
): ReturnType<typeof resolveWhatsAppHeartbeatRecipients> {
|
||||
return resolveWhatsAppHeartbeatRecipients(...args);
|
||||
...args: Parameters<WhatsAppHeavyRuntimeModule["resolveHeartbeatRecipients"]>
|
||||
): ReturnType<WhatsAppHeavyRuntimeModule["resolveHeartbeatRecipients"]> {
|
||||
return loadCurrentHeavyModuleSync().resolveHeartbeatRecipients(...args);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { createRuntimeWhatsAppLoginTool } from "./runtime-whatsapp-boundary.js";
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ChannelDirectoryAdapter } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import type { ChannelDirectoryAdapter } from "openclaw/plugin-sdk/channel-contract";
|
||||
|
||||
type DirectorySurface = {
|
||||
listPeers: NonNullable<ChannelDirectoryAdapter["listPeers"]>;
|
||||
|
||||
Reference in New Issue
Block a user