mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-30 19:00:23 +00:00
refactor: converge plugin sdk channel helpers
This commit is contained in:
@@ -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<void> {
|
||||
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<void> {
|
||||
const { account, config, runtime, core } = target;
|
||||
const pairing = createScopedPairingAccess({
|
||||
const pairing = createChannelPairingController({
|
||||
core,
|
||||
channel: "bluebubbles",
|
||||
accountId: account.accountId,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export type { SecretInput } from "openclaw/plugin-sdk/secret-input";
|
||||
export {
|
||||
buildSecretInputSchema,
|
||||
hasConfiguredSecretInput,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user