refactor(plugin-sdk): genericize web channel runtime seams

This commit is contained in:
Peter Steinberger
2026-04-03 11:16:17 +01:00
parent 182bec5091
commit 2766c27b2a
70 changed files with 490 additions and 265 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -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");

View File

@@ -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";

View File

@@ -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,

View File

@@ -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();

View File

@@ -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")>();

View File

@@ -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 {

View File

@@ -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";

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,
}));

View File

@@ -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";

View File

@@ -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 = {

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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>;

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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

View File

@@ -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>;

View File

@@ -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";

View File

@@ -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")>();

View File

@@ -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";

View File

@@ -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>;

View File

@@ -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";

View File

@@ -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>;

View File

@@ -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";

View File

@@ -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,

View File

@@ -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,

View File

@@ -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";

View File

@@ -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";

View File

@@ -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,

View File

@@ -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";

View File

@@ -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,

View File

@@ -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,

View File

@@ -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";

View 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")];
}

View File

@@ -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([]);
});

View 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" };
}

View File

@@ -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";

View File

@@ -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),

View File

@@ -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";

View File

@@ -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
);
}

View File

@@ -2,8 +2,6 @@ export {
looksLikeWhatsAppTargetId,
normalizeWhatsAppAllowFromEntries,
normalizeWhatsAppMessagingTarget,
} from "./runtime-api.js";
export {
isWhatsAppGroupJid,
isWhatsAppUserTarget,
normalizeWhatsAppTarget,

View 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,
}),
}),
};
}

View File

@@ -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";

View File

@@ -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,

View File

@@ -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.

View File

@@ -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(),

View File

@@ -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", () => {

View File

@@ -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 = {

View File

@@ -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 })),

View File

@@ -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,

View File

@@ -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),
}));

View File

@@ -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;

View File

@@ -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 = {

View File

@@ -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() {

View File

@@ -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;
}

View File

@@ -18,3 +18,5 @@ export type {
ChannelThreadingContext,
ChannelThreadingToolContext,
} from "../channels/plugins/types.js";
export type { ChannelDirectoryAdapter } from "../channels/plugins/types.adapters.js";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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,

View File

@@ -1 +0,0 @@
export { handleWhatsAppAction } from "../plugins/runtime/runtime-whatsapp-boundary.js";

View File

@@ -1,4 +0,0 @@
export {
startWebLoginWithQr,
waitForWebLogin,
} from "../plugins/runtime/runtime-whatsapp-boundary.js";

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -1 +0,0 @@
export { createRuntimeWhatsAppLoginTool } from "./runtime-whatsapp-boundary.js";

View File

@@ -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"]>;