From c70837f07d1f2e8ab6ea44e08acddd64395331b6 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 19 Mar 2026 00:25:12 +0000 Subject: [PATCH] refactor: converge plugin sdk channel helpers --- .../bluebubbles/src/monitor-processing.ts | 77 ++++++++-------- extensions/kilocode/onboard.ts | 25 +++--- extensions/mattermost/src/channel.test.ts | 4 +- .../mattermost/src/mattermost/monitor.ts | 90 +++++++++---------- .../mattermost/src/mattermost/slash-http.ts | 32 ++++--- extensions/mattermost/src/secret-input.ts | 1 + extensions/mattermost/src/setup-core.ts | 2 +- extensions/mattermost/src/setup-surface.ts | 2 +- extensions/mattermost/src/types.ts | 8 +- .../src/monitor-handler/message-handler.ts | 4 +- extensions/msteams/src/reply-dispatcher.ts | 29 +++--- src/plugin-sdk/bluebubbles.ts | 18 +--- src/plugin-sdk/channel-reply-pipeline.test.ts | 20 +++++ src/plugin-sdk/channel-reply-pipeline.ts | 7 +- src/plugin-sdk/mattermost.ts | 12 +-- src/plugin-sdk/msteams.ts | 5 +- 16 files changed, 166 insertions(+), 170 deletions(-) diff --git a/extensions/bluebubbles/src/monitor-processing.ts b/extensions/bluebubbles/src/monitor-processing.ts index ef01150487b..b0c4ce8d324 100644 --- a/extensions/bluebubbles/src/monitor-processing.ts +++ b/extensions/bluebubbles/src/monitor-processing.ts @@ -38,10 +38,9 @@ import { normalizeBlueBubblesReactionInput, sendBlueBubblesReaction } from "./re import type { OpenClawConfig } from "./runtime-api.js"; import { DM_GROUP_ACCESS_REASON, - createScopedPairingAccess, - createReplyPrefixOptions, + createChannelPairingController, + createChannelReplyPipeline, evictOldHistoryKeys, - issuePairingChallenge, logAckFailure, logInboundDrop, logTypingFailure, @@ -452,7 +451,7 @@ export async function processMessage( target: WebhookTarget, ): Promise { const { account, config, runtime, core, statusSink } = target; - const pairing = createScopedPairingAccess({ + const pairing = createChannelPairingController({ core, channel: "bluebubbles", accountId: account.accountId, @@ -654,12 +653,10 @@ export async function processMessage( } if (accessDecision.decision === "pairing") { - await issuePairingChallenge({ - channel: "bluebubbles", + await pairing.issueChallenge({ senderId: message.senderId, senderIdLine: `Your BlueBubbles sender id: ${message.senderId}`, meta: { name: message.senderName }, - upsertPairingRequest: pairing.upsertPairingRequest, onCreated: () => { runtime.log?.(`[bluebubbles] pairing request sender=${message.senderId} created=true`); logVerbose(core, runtime, `bluebubbles pairing request sender=${message.senderId}`); @@ -1228,17 +1225,47 @@ export async function processMessage( }, typingRestartDelayMs); }; try { - const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({ + const { onModelSelected, typingCallbacks, ...replyPipeline } = createChannelReplyPipeline({ cfg: config, agentId: route.agentId, channel: "bluebubbles", accountId: account.accountId, + typingCallbacks: { + onReplyStart: async () => { + if (!chatGuidForActions) { + return; + } + if (!baseUrl || !password) { + return; + } + streamingActive = true; + clearTypingRestartTimer(); + try { + await sendBlueBubblesTyping(chatGuidForActions, true, { + cfg: config, + accountId: account.accountId, + }); + } catch (err) { + runtime.error?.(`[bluebubbles] typing start failed: ${String(err)}`); + } + }, + onIdle: () => { + if (!chatGuidForActions) { + return; + } + if (!baseUrl || !password) { + return; + } + // Intentionally no-op for block streaming. We stop typing in finally + // after the run completes to avoid flicker between paragraph blocks. + }, + }, }); await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({ ctx: ctxPayload, cfg: config, dispatcherOptions: { - ...prefixOptions, + ...replyPipeline, deliver: async (payload, info) => { const rawReplyToId = privateApiEnabled && typeof payload.replyToId === "string" @@ -1356,34 +1383,8 @@ export async function processMessage( } } }, - onReplyStart: async () => { - if (!chatGuidForActions) { - return; - } - if (!baseUrl || !password) { - return; - } - streamingActive = true; - clearTypingRestartTimer(); - try { - await sendBlueBubblesTyping(chatGuidForActions, true, { - cfg: config, - accountId: account.accountId, - }); - } catch (err) { - runtime.error?.(`[bluebubbles] typing start failed: ${String(err)}`); - } - }, - onIdle: async () => { - if (!chatGuidForActions) { - return; - } - if (!baseUrl || !password) { - return; - } - // Intentionally no-op for block streaming. We stop typing in finally - // after the run completes to avoid flicker between paragraph blocks. - }, + onReplyStart: typingCallbacks?.onReplyStart, + onIdle: typingCallbacks?.onIdle, onError: (err, info) => { runtime.error?.(`BlueBubbles ${info.kind} reply failed: ${String(err)}`); }, @@ -1447,7 +1448,7 @@ export async function processReaction( target: WebhookTarget, ): Promise { const { account, config, runtime, core } = target; - const pairing = createScopedPairingAccess({ + const pairing = createChannelPairingController({ core, channel: "bluebubbles", accountId: account.accountId, diff --git a/extensions/kilocode/onboard.ts b/extensions/kilocode/onboard.ts index fd285341f52..88533dd64a0 100644 --- a/extensions/kilocode/onboard.ts +++ b/extensions/kilocode/onboard.ts @@ -1,7 +1,6 @@ import { KILOCODE_BASE_URL, KILOCODE_DEFAULT_MODEL_REF } from "openclaw/plugin-sdk/provider-models"; import { - applyAgentDefaultModelPrimary, - applyProviderConfigWithModelCatalog, + applyProviderConfigWithModelCatalogPreset, type OpenClawConfig, } from "openclaw/plugin-sdk/provider-onboard"; import { buildKilocodeProvider } from "./provider-catalog.js"; @@ -9,24 +8,22 @@ import { buildKilocodeProvider } from "./provider-catalog.js"; export { KILOCODE_BASE_URL, KILOCODE_DEFAULT_MODEL_REF }; export function applyKilocodeProviderConfig(cfg: OpenClawConfig): OpenClawConfig { - const models = { ...cfg.agents?.defaults?.models }; - models[KILOCODE_DEFAULT_MODEL_REF] = { - ...models[KILOCODE_DEFAULT_MODEL_REF], - alias: models[KILOCODE_DEFAULT_MODEL_REF]?.alias ?? "Kilo Gateway", - }; - - return applyProviderConfigWithModelCatalog(cfg, { - agentModels: models, + return applyProviderConfigWithModelCatalogPreset(cfg, { providerId: "kilocode", api: "openai-completions", baseUrl: KILOCODE_BASE_URL, catalogModels: buildKilocodeProvider().models ?? [], + aliases: [{ modelRef: KILOCODE_DEFAULT_MODEL_REF, alias: "Kilo Gateway" }], }); } export function applyKilocodeConfig(cfg: OpenClawConfig): OpenClawConfig { - return applyAgentDefaultModelPrimary( - applyKilocodeProviderConfig(cfg), - KILOCODE_DEFAULT_MODEL_REF, - ); + return applyProviderConfigWithModelCatalogPreset(cfg, { + providerId: "kilocode", + api: "openai-completions", + baseUrl: KILOCODE_BASE_URL, + catalogModels: buildKilocodeProvider().models ?? [], + aliases: [{ modelRef: KILOCODE_DEFAULT_MODEL_REF, alias: "Kilo Gateway" }], + primaryModelRef: KILOCODE_DEFAULT_MODEL_REF, + }); } diff --git a/extensions/mattermost/src/channel.test.ts b/extensions/mattermost/src/channel.test.ts index 4b66bf05edd..ea8e52024ca 100644 --- a/extensions/mattermost/src/channel.test.ts +++ b/extensions/mattermost/src/channel.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../runtime-api.js"; -import { createReplyPrefixOptions } from "../runtime-api.js"; +import { createChannelReplyPipeline } from "../runtime-api.js"; const { sendMessageMattermostMock } = vi.hoisted(() => ({ sendMessageMattermostMock: vi.fn(), })); @@ -431,7 +431,7 @@ describe("mattermostPlugin", () => { }, }; - const prefixContext = createReplyPrefixOptions({ + const prefixContext = createChannelReplyPipeline({ cfg, agentId: "main", channel: "mattermost", diff --git a/extensions/mattermost/src/mattermost/monitor.ts b/extensions/mattermost/src/mattermost/monitor.ts index 1d1f81bf0a1..958a40de705 100644 --- a/extensions/mattermost/src/mattermost/monitor.ts +++ b/extensions/mattermost/src/mattermost/monitor.ts @@ -9,9 +9,8 @@ import { buildAgentMediaPayload, buildModelsProviderData, DM_GROUP_ACCESS_REASON, - createScopedPairingAccess, - createReplyPrefixOptions, - createTypingCallbacks, + createChannelPairingController, + createChannelReplyPipeline, logInboundDrop, logTypingFailure, buildPendingHistoryContextFromMap, @@ -245,7 +244,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} cfg, accountId: opts.accountId, }); - const pairing = createScopedPairingAccess({ + const pairing = createChannelPairingController({ core, channel: "mattermost", accountId: account.accountId, @@ -462,26 +461,26 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} channel: "mattermost", accountId: account.accountId, }); - const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({ + const { onModelSelected, typingCallbacks, ...replyPipeline } = createChannelReplyPipeline({ cfg, agentId: route.agentId, channel: "mattermost", accountId: account.accountId, - }); - const typingCallbacks = createTypingCallbacks({ - start: () => sendTypingIndicator(opts.channelId, threadContext.effectiveReplyToId), - onStartError: (err) => { - logTypingFailure({ - log: (message) => logger.debug?.(message), - channel: "mattermost", - target: opts.channelId, - error: err, - }); + typing: { + start: () => sendTypingIndicator(opts.channelId, threadContext.effectiveReplyToId), + onStartError: (err) => { + logTypingFailure({ + log: (message) => logger.debug?.(message), + channel: "mattermost", + target: opts.channelId, + error: err, + }); + }, }, }); const { dispatcher, replyOptions, markDispatchIdle } = core.channel.reply.createReplyDispatcherWithTyping({ - ...prefixOptions, + ...replyPipeline, humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, route.agentId), deliver: async (payload: ReplyPayload) => { await deliverMattermostReplyPayload({ @@ -504,7 +503,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} onError: (err, info) => { runtime.error?.(`mattermost button-click ${info.kind} reply failed: ${String(err)}`); }, - onReplyStart: typingCallbacks.onReplyStart, + onReplyStart: typingCallbacks?.onReplyStart, }); await core.channel.reply.dispatchReplyFromConfig({ @@ -653,30 +652,30 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} fallbackLimit: account.textChunkLimit ?? 4000, }, ); - const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({ + const shouldDeliverReplies = params.deliverReplies === true; + const { onModelSelected, typingCallbacks, ...replyPipeline } = createChannelReplyPipeline({ cfg, agentId: params.route.agentId, channel: "mattermost", accountId: account.accountId, + typing: shouldDeliverReplies + ? { + start: () => sendTypingIndicator(params.channelId, params.effectiveReplyToId), + onStartError: (err) => { + logTypingFailure({ + log: (message) => logger.debug?.(message), + channel: "mattermost", + target: params.channelId, + error: err, + }); + }, + } + : undefined, }); - const shouldDeliverReplies = params.deliverReplies === true; const capturedTexts: string[] = []; - const typingCallbacks = shouldDeliverReplies - ? createTypingCallbacks({ - start: () => sendTypingIndicator(params.channelId, params.effectiveReplyToId), - onStartError: (err) => { - logTypingFailure({ - log: (message) => logger.debug?.(message), - channel: "mattermost", - target: params.channelId, - error: err, - }); - }, - }) - : undefined; const { dispatcher, replyOptions, markDispatchIdle } = core.channel.reply.createReplyDispatcherWithTyping({ - ...prefixOptions, + ...replyPipeline, // Picker-triggered confirmations should stay immediate. deliver: async (payload: ReplyPayload) => { const trimmedPayload = { @@ -1379,27 +1378,26 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} accountId: account.accountId, }); - const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({ + const { onModelSelected, typingCallbacks, ...replyPipeline } = createChannelReplyPipeline({ cfg, agentId: route.agentId, channel: "mattermost", accountId: account.accountId, - }); - - const typingCallbacks = createTypingCallbacks({ - start: () => sendTypingIndicator(channelId, effectiveReplyToId), - onStartError: (err) => { - logTypingFailure({ - log: (message) => logger.debug?.(message), - channel: "mattermost", - target: channelId, - error: err, - }); + typing: { + start: () => sendTypingIndicator(channelId, effectiveReplyToId), + onStartError: (err) => { + logTypingFailure({ + log: (message) => logger.debug?.(message), + channel: "mattermost", + target: channelId, + error: err, + }); + }, }, }); const { dispatcher, replyOptions, markDispatchIdle } = core.channel.reply.createReplyDispatcherWithTyping({ - ...prefixOptions, + ...replyPipeline, humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, route.agentId), typingCallbacks, deliver: async (payload: ReplyPayload) => { diff --git a/extensions/mattermost/src/mattermost/slash-http.ts b/extensions/mattermost/src/mattermost/slash-http.ts index 4d4d5f502a3..374af5da044 100644 --- a/extensions/mattermost/src/mattermost/slash-http.ts +++ b/extensions/mattermost/src/mattermost/slash-http.ts @@ -9,8 +9,7 @@ import type { IncomingMessage, ServerResponse } from "node:http"; import type { ResolvedMattermostAccount } from "../mattermost/accounts.js"; import { buildModelsProviderData, - createReplyPrefixOptions, - createTypingCallbacks, + createChannelReplyPipeline, isRequestBodyLimitError, logTypingFailure, readRequestBodyWithLimit, @@ -466,29 +465,28 @@ async function handleSlashCommandAsync(params: { accountId: account.accountId, }); - const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({ + const { onModelSelected, typingCallbacks, ...replyPipeline } = createChannelReplyPipeline({ cfg, agentId: route.agentId, channel: "mattermost", accountId: account.accountId, + typing: { + start: () => sendMattermostTyping(client, { channelId }), + onStartError: (err) => { + logTypingFailure({ + log: (message) => log?.(message), + channel: "mattermost", + target: channelId, + error: err, + }); + }, + }, }); const humanDelay = core.channel.reply.resolveHumanDelayConfig(cfg, route.agentId); - const typingCallbacks = createTypingCallbacks({ - start: () => sendMattermostTyping(client, { channelId }), - onStartError: (err) => { - logTypingFailure({ - log: (message) => log?.(message), - channel: "mattermost", - target: channelId, - error: err, - }); - }, - }); - const { dispatcher, replyOptions, markDispatchIdle } = core.channel.reply.createReplyDispatcherWithTyping({ - ...prefixOptions, + ...replyPipeline, humanDelay, deliver: async (payload: ReplyPayload) => { await deliverMattermostReplyPayload({ @@ -507,7 +505,7 @@ async function handleSlashCommandAsync(params: { onError: (err, info) => { runtime.error?.(`mattermost slash ${info.kind} reply failed: ${String(err)}`); }, - onReplyStart: typingCallbacks.onReplyStart, + onReplyStart: typingCallbacks?.onReplyStart, }); await core.channel.reply.withReplyDispatcher({ diff --git a/extensions/mattermost/src/secret-input.ts b/extensions/mattermost/src/secret-input.ts index f1b2aae5c92..d8d7aaf31d2 100644 --- a/extensions/mattermost/src/secret-input.ts +++ b/extensions/mattermost/src/secret-input.ts @@ -1,3 +1,4 @@ +export type { SecretInput } from "openclaw/plugin-sdk/secret-input"; export { buildSecretInputSchema, hasConfiguredSecretInput, diff --git a/extensions/mattermost/src/setup-core.ts b/extensions/mattermost/src/setup-core.ts index 624a31a48c4..36954819fd5 100644 --- a/extensions/mattermost/src/setup-core.ts +++ b/extensions/mattermost/src/setup-core.ts @@ -5,11 +5,11 @@ import { applyAccountNameToChannelSection, applySetupAccountConfigPatch, DEFAULT_ACCOUNT_ID, - hasConfiguredSecretInput, migrateBaseNameToDefaultAccount, normalizeAccountId, type OpenClawConfig, } from "./runtime-api.js"; +import { hasConfiguredSecretInput } from "./secret-input.js"; const channel = "mattermost" as const; diff --git a/extensions/mattermost/src/setup-surface.ts b/extensions/mattermost/src/setup-surface.ts index a439dd15006..dd09e3a1492 100644 --- a/extensions/mattermost/src/setup-surface.ts +++ b/extensions/mattermost/src/setup-surface.ts @@ -5,9 +5,9 @@ import { normalizeMattermostBaseUrl } from "./mattermost/client.js"; import { applySetupAccountConfigPatch, DEFAULT_ACCOUNT_ID, - hasConfiguredSecretInput, type OpenClawConfig, } from "./runtime-api.js"; +import { hasConfiguredSecretInput } from "./secret-input.js"; import { isMattermostConfigured, mattermostSetupAdapter, diff --git a/extensions/mattermost/src/types.ts b/extensions/mattermost/src/types.ts index b77a542122b..77ad9461803 100644 --- a/extensions/mattermost/src/types.ts +++ b/extensions/mattermost/src/types.ts @@ -1,9 +1,5 @@ -import type { - BlockStreamingCoalesceConfig, - DmPolicy, - GroupPolicy, - SecretInput, -} from "./runtime-api.js"; +import type { BlockStreamingCoalesceConfig, DmPolicy, GroupPolicy } from "./runtime-api.js"; +import type { SecretInput } from "./secret-input.js"; export type MattermostReplyToMode = "off" | "first" | "all"; export type MattermostChatTypeKey = "direct" | "channel" | "group"; diff --git a/extensions/msteams/src/monitor-handler/message-handler.ts b/extensions/msteams/src/monitor-handler/message-handler.ts index d07050062df..8f71e80bbf2 100644 --- a/extensions/msteams/src/monitor-handler/message-handler.ts +++ b/extensions/msteams/src/monitor-handler/message-handler.ts @@ -2,9 +2,9 @@ import { DEFAULT_ACCOUNT_ID, buildPendingHistoryContextFromMap, clearHistoryEntriesIfEnabled, + createChannelPairingController, dispatchReplyFromConfigWithSettledDispatcher, DEFAULT_GROUP_HISTORY_LIMIT, - createScopedPairingAccess, logInboundDrop, evaluateSenderGroupAccessForPolicy, resolveSenderScopedGroupPolicy, @@ -63,7 +63,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { log, } = deps; const core = getMSTeamsRuntime(); - const pairing = createScopedPairingAccess({ + const pairing = createChannelPairingController({ core, channel: "msteams", accountId: DEFAULT_ACCOUNT_ID, diff --git a/extensions/msteams/src/reply-dispatcher.ts b/extensions/msteams/src/reply-dispatcher.ts index 80540d9c527..a16d2185319 100644 --- a/extensions/msteams/src/reply-dispatcher.ts +++ b/extensions/msteams/src/reply-dispatcher.ts @@ -1,6 +1,5 @@ import { - createReplyPrefixOptions, - createTypingCallbacks, + createChannelReplyPipeline, logTypingFailure, resolveChannelMediaMaxBytes, type OpenClawConfig, @@ -73,28 +72,28 @@ export function createMSTeamsReplyDispatcher(params: { }); }; - const typingCallbacks = createTypingCallbacks({ - start: sendTypingIndicator, - onStartError: (err) => { - logTypingFailure({ - log: (message) => params.log.debug?.(message), - channel: "msteams", - action: "start", - error: err, - }); - }, - }); - const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({ + const { onModelSelected, typingCallbacks, ...replyPipeline } = createChannelReplyPipeline({ cfg: params.cfg, agentId: params.agentId, channel: "msteams", accountId: params.accountId, + typing: { + start: sendTypingIndicator, + onStartError: (err) => { + logTypingFailure({ + log: (message) => params.log.debug?.(message), + channel: "msteams", + action: "start", + error: err, + }); + }, + }, }); const chunkMode = core.channel.text.resolveChunkMode(params.cfg, "msteams"); const { dispatcher, replyOptions, markDispatchIdle } = core.channel.reply.createReplyDispatcherWithTyping({ - ...prefixOptions, + ...replyPipeline, humanDelay: core.channel.reply.resolveHumanDelayConfig(params.cfg, params.agentId), typingCallbacks, deliver: async (payload) => { diff --git a/src/plugin-sdk/bluebubbles.ts b/src/plugin-sdk/bluebubbles.ts index 58438157dda..ac76dcc29a3 100644 --- a/src/plugin-sdk/bluebubbles.ts +++ b/src/plugin-sdk/bluebubbles.ts @@ -51,15 +51,9 @@ export type { ChannelMessageActionName, } from "../channels/plugins/types.js"; export type { ChannelPlugin } from "../channels/plugins/types.plugin.js"; -export { createReplyPrefixOptions } from "../channels/reply-prefix.js"; +export { createChannelReplyPipeline } from "./channel-reply-pipeline.js"; export type { OpenClawConfig } from "../config/config.js"; export type { DmPolicy, GroupPolicy } from "../config/types.js"; -export { - hasConfiguredSecretInput, - normalizeResolvedSecretInputString, - normalizeSecretInputString, -} from "../config/types.secrets.js"; -export { buildSecretInputSchema } from "./secret-input-schema.js"; export { ToolPolicySchema } from "../config/zod-schema.agent-runtime.js"; export { MarkdownConfigSchema } from "../config/zod-schema.core.js"; export type { ParsedChatTarget } from "../../extensions/imessage/api.js"; @@ -85,23 +79,19 @@ export type { WizardPrompter } from "../wizard/prompts.js"; export { isAllowedParsedChatSender } from "./allow-from.js"; export { readBooleanParam } from "./boolean-param.js"; export { mapAllowFromEntries } from "./channel-config-helpers.js"; -export { createScopedPairingAccess } from "./pairing-access.js"; -export { issuePairingChallenge } from "../pairing/pairing-challenge.js"; +export { createChannelPairingController } from "./channel-pairing.js"; export { resolveRequestUrl } from "./request-url.js"; export { buildComputedAccountStatusSnapshot, buildProbeChannelStatusSummary, } from "./status-helpers.js"; export { extractToolSend } from "./tool-send.js"; -export { normalizeWebhookPath } from "./webhook-path.js"; export { - beginWebhookRequestPipelineOrReject, createWebhookInFlightLimiter, + normalizeWebhookPath, readWebhookBodyOrReject, -} from "./webhook-request-guards.js"; -export { registerWebhookTargetWithPluginRoute, resolveWebhookTargets, resolveWebhookTargetWithAuthOrRejectSync, withResolvedWebhookRequestPipeline, -} from "./webhook-targets.js"; +} from "./webhook-ingress.js"; diff --git a/src/plugin-sdk/channel-reply-pipeline.test.ts b/src/plugin-sdk/channel-reply-pipeline.test.ts index cc8c15e4b16..ae94736df3d 100644 --- a/src/plugin-sdk/channel-reply-pipeline.test.ts +++ b/src/plugin-sdk/channel-reply-pipeline.test.ts @@ -36,4 +36,24 @@ describe("createChannelReplyPipeline", () => { expect(start).toHaveBeenCalled(); expect(stop).toHaveBeenCalled(); }); + + it("preserves explicit typing callbacks when a channel needs custom lifecycle hooks", async () => { + const onReplyStart = vi.fn(async () => {}); + const onIdle = vi.fn(() => {}); + const pipeline = createChannelReplyPipeline({ + cfg: {}, + agentId: "main", + channel: "bluebubbles", + typingCallbacks: { + onReplyStart, + onIdle, + }, + }); + + await pipeline.typingCallbacks?.onReplyStart(); + pipeline.typingCallbacks?.onIdle?.(); + + expect(onReplyStart).toHaveBeenCalledTimes(1); + expect(onIdle).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/plugin-sdk/channel-reply-pipeline.ts b/src/plugin-sdk/channel-reply-pipeline.ts index a2244ade7f1..6bbb04f5409 100644 --- a/src/plugin-sdk/channel-reply-pipeline.ts +++ b/src/plugin-sdk/channel-reply-pipeline.ts @@ -25,6 +25,7 @@ export function createChannelReplyPipeline(params: { channel?: string; accountId?: string; typing?: CreateTypingCallbacksParams; + typingCallbacks?: TypingCallbacks; }): ChannelReplyPipeline { return { ...createReplyPrefixOptions({ @@ -33,6 +34,10 @@ export function createChannelReplyPipeline(params: { channel: params.channel, accountId: params.accountId, }), - ...(params.typing ? { typingCallbacks: createTypingCallbacks(params.typing) } : {}), + ...(params.typingCallbacks + ? { typingCallbacks: params.typingCallbacks } + : params.typing + ? { typingCallbacks: createTypingCallbacks(params.typing) } + : {}), }; } diff --git a/src/plugin-sdk/mattermost.ts b/src/plugin-sdk/mattermost.ts index c8043045906..8ab28d2a4ea 100644 --- a/src/plugin-sdk/mattermost.ts +++ b/src/plugin-sdk/mattermost.ts @@ -50,8 +50,7 @@ export type { } from "../channels/plugins/types.js"; export type { ChannelDirectoryEntry } from "../channels/plugins/types.core.js"; export type { ChannelPlugin } from "../channels/plugins/types.plugin.js"; -export { createReplyPrefixOptions } from "../channels/reply-prefix.js"; -export { createTypingCallbacks } from "../channels/typing.js"; +export { createChannelReplyPipeline } from "./channel-reply-pipeline.js"; export type { OpenClawConfig } from "../config/config.js"; export { isDangerousNameMatchingEnabled } from "../config/dangerous-name-matching.js"; export { loadSessionStore, resolveStorePath } from "../config/sessions.js"; @@ -61,13 +60,6 @@ export { warnMissingProviderGroupPolicyFallbackOnce, } from "../config/runtime-group-policy.js"; export type { BlockStreamingCoalesceConfig, DmPolicy, GroupPolicy } from "../config/types.js"; -export type { SecretInput } from "../config/types.secrets.js"; -export { - hasConfiguredSecretInput, - normalizeResolvedSecretInputString, - normalizeSecretInputString, -} from "../config/types.secrets.js"; -export { buildSecretInputSchema } from "./secret-input-schema.js"; export { BlockStreamingCoalesceSchema, DmPolicySchema, @@ -100,5 +92,5 @@ export type { WizardPrompter } from "../wizard/prompts.js"; export { buildAgentMediaPayload } from "./agent-media-payload.js"; export { getAgentScopedMediaLocalRoots } from "../media/local-roots.js"; export { loadOutboundMediaFromUrl } from "./outbound-media.js"; -export { createScopedPairingAccess } from "./pairing-access.js"; +export { createChannelPairingController } from "./channel-pairing.js"; export { isRequestBodyLimitError, readRequestBodyWithLimit } from "../infra/http-body.js"; diff --git a/src/plugin-sdk/msteams.ts b/src/plugin-sdk/msteams.ts index a48843137a0..1c72c82ea53 100644 --- a/src/plugin-sdk/msteams.ts +++ b/src/plugin-sdk/msteams.ts @@ -52,8 +52,7 @@ export type { ChannelOutboundAdapter, } from "../channels/plugins/types.js"; export type { ChannelPlugin } from "../channels/plugins/types.plugin.js"; -export { createChannelReplyPipeline, createReplyPrefixOptions } from "./channel-reply-pipeline.js"; -export { createTypingCallbacks } from "./channel-reply-pipeline.js"; +export { createChannelReplyPipeline } from "./channel-reply-pipeline.js"; export type { OpenClawConfig } from "../config/config.js"; export { isDangerousNameMatchingEnabled } from "../config/dangerous-name-matching.js"; export { resolveToolsBySender } from "../config/group-policy.js"; @@ -106,7 +105,7 @@ export { withFileLock } from "./file-lock.js"; export { dispatchReplyFromConfigWithSettledDispatcher } from "./inbound-reply-dispatch.js"; export { readJsonFileWithFallback, writeJsonFileAtomically } from "./json-store.js"; export { loadOutboundMediaFromUrl } from "./outbound-media.js"; -export { createChannelPairingController, createScopedPairingAccess } from "./channel-pairing.js"; +export { createChannelPairingController } from "./channel-pairing.js"; export { resolveInboundSessionEnvelopeContext } from "../channels/session-envelope.js"; export { buildHostnameAllowlistPolicyFromSuffixAllowlist,