refactor: share slack and telegram action helpers

This commit is contained in:
Peter Steinberger
2026-03-26 19:07:35 +00:00
parent a1a9819be8
commit 8f1716ae5a
9 changed files with 129 additions and 172 deletions

View File

@@ -0,0 +1,30 @@
import { parseSlackTarget } from "./targets.js";
export function resolveSlackAutoThreadId(params: {
to: string;
toolContext?: {
currentChannelId?: string;
currentThreadTs?: string;
replyToMode?: "off" | "first" | "all";
hasRepliedRef?: { value: boolean };
};
}): string | undefined {
const context = params.toolContext;
if (!context?.currentThreadTs || !context.currentChannelId) {
return undefined;
}
if (context.replyToMode !== "all" && context.replyToMode !== "first") {
return undefined;
}
const parsedTarget = parseSlackTarget(params.to, { defaultKind: "channel" });
if (!parsedTarget || parsedTarget.kind !== "channel") {
return undefined;
}
if (parsedTarget.id.toLowerCase() !== context.currentChannelId.toLowerCase()) {
return undefined;
}
if (context.replyToMode === "first" && context.hasRepliedRef?.value) {
return undefined;
}
return context.currentThreadTs;
}

View File

@@ -37,6 +37,7 @@ import {
type ResolvedSlackAccount,
} from "./accounts.js";
import type { SlackActionContext } from "./action-runtime.js";
import { resolveSlackAutoThreadId } from "./action-threading.js";
import { parseSlackBlocksInput } from "./blocks-input.js";
import { createSlackActions } from "./channel-actions.js";
import { resolveSlackChannelType } from "./channel-type.js";
@@ -122,37 +123,6 @@ function resolveSlackSendContext(params: {
return { send, threadTsValue, tokenOverride };
}
function resolveSlackAutoThreadId(params: {
cfg: Parameters<typeof resolveSlackAccount>[0]["cfg"];
accountId?: string | null;
to: string;
toolContext?: {
currentChannelId?: string;
currentThreadTs?: string;
replyToMode?: "off" | "first" | "all";
hasRepliedRef?: { value: boolean };
};
}): string | undefined {
const context = params.toolContext;
if (!context?.currentThreadTs || !context.currentChannelId) {
return undefined;
}
if (context.replyToMode !== "all" && context.replyToMode !== "first") {
return undefined;
}
const parsedTarget = parseSlackTarget(params.to, { defaultKind: "channel" });
if (!parsedTarget || parsedTarget.kind !== "channel") {
return undefined;
}
if (parsedTarget.id.toLowerCase() !== context.currentChannelId.toLowerCase()) {
return undefined;
}
if (context.replyToMode === "first" && context.hasRepliedRef?.value) {
return undefined;
}
return context.currentThreadTs;
}
function parseSlackExplicitTarget(raw: string) {
const target = parseSlackTarget(raw, { defaultKind: "channel" });
if (!target) {
@@ -520,12 +490,10 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount, SlackProbe> = crea
},
allowExplicitReplyTagsWhenOff: false,
buildToolContext: (params) => buildSlackThreadingToolContext(params),
resolveAutoThreadId: ({ cfg, accountId, to, toolContext, replyToId }) =>
resolveAutoThreadId: ({ to, toolContext, replyToId }) =>
replyToId
? undefined
: resolveSlackAutoThreadId({
cfg,
accountId,
to,
toolContext,
}),

View File

@@ -0,0 +1,17 @@
import { parseTelegramTarget } from "./targets.js";
export function resolveTelegramAutoThreadId(params: {
to: string;
toolContext?: { currentThreadTs?: string; currentChannelId?: string };
}): string | undefined {
const context = params.toolContext;
if (!context?.currentThreadTs || !context.currentChannelId) {
return undefined;
}
const parsedTo = parseTelegramTarget(params.to);
const parsedChannel = parseTelegramTarget(context.currentChannelId);
if (parsedTo.chatId.toLowerCase() !== parsedChannel.chatId.toLowerCase()) {
return undefined;
}
return context.currentThreadTs;
}

View File

@@ -7,8 +7,6 @@ import { createAllowlistProviderRouteAllowlistWarningCollector } from "openclaw/
import { attachChannelToResult } from "openclaw/plugin-sdk/channel-send-result";
import { createChatChannelPlugin } from "openclaw/plugin-sdk/core";
import { createChannelDirectoryAdapter } from "openclaw/plugin-sdk/directory-runtime";
import { resolveExecApprovalCommandDisplay } from "openclaw/plugin-sdk/infra-runtime";
import { buildExecApprovalPendingReplyPayload } from "openclaw/plugin-sdk/infra-runtime";
import {
resolveOutboundSendDep,
type OutboundSendDeps,
@@ -40,13 +38,17 @@ import {
resolveTelegramAccount,
type ResolvedTelegramAccount,
} from "./accounts.js";
import { buildTelegramExecApprovalButtons } from "./approval-buttons.js";
import { resolveTelegramAutoThreadId } from "./action-threading.js";
import { auditTelegramGroupMembership, collectTelegramUnmentionedGroupIds } from "./audit.js";
import { buildTelegramGroupPeerId } from "./bot/helpers.js";
import {
listTelegramDirectoryGroupsFromConfig,
listTelegramDirectoryPeersFromConfig,
} from "./directory-config.js";
import {
buildTelegramExecApprovalPendingPayload,
shouldSuppressTelegramExecApprovalForwardingFallback,
} from "./exec-approval-forwarding.js";
import {
isTelegramExecApprovalClientEnabled,
resolveTelegramExecApprovalTarget,
@@ -138,22 +140,6 @@ async function sendTelegramOutbound(params: {
);
}
function resolveTelegramAutoThreadId(params: {
to: string;
toolContext?: { currentThreadTs?: string; currentChannelId?: string };
}): string | undefined {
const context = params.toolContext;
if (!context?.currentThreadTs || !context.currentChannelId) {
return undefined;
}
const parsedTo = parseTelegramTarget(params.to);
const parsedChannel = parseTelegramTarget(context.currentChannelId);
if (parsedTo.chatId.toLowerCase() !== parsedChannel.chatId.toLowerCase()) {
return undefined;
}
return context.currentThreadTs;
}
function normalizeTelegramAcpConversationId(conversationId: string) {
const parsed = parseTelegramTopicConversation({ conversationId });
if (!parsed || !parsed.chatId.startsWith("-")) {
@@ -393,44 +379,10 @@ export const telegramPlugin = createChatChannelPlugin({
? { kind: "enabled" }
: { kind: "disabled" },
hasConfiguredDmRoute: ({ cfg }) => hasTelegramExecApprovalDmRoute(cfg),
shouldSuppressForwardingFallback: ({ cfg, target, request }) => {
const channel = normalizeMessageChannel(target.channel) ?? target.channel;
if (channel !== "telegram") {
return false;
}
const requestChannel = normalizeMessageChannel(request.request.turnSourceChannel ?? "");
if (requestChannel !== "telegram") {
return false;
}
const accountId = target.accountId?.trim() || request.request.turnSourceAccountId?.trim();
return isTelegramExecApprovalClientEnabled({ cfg, accountId });
},
buildPendingPayload: ({ request, nowMs }) => {
const payload = buildExecApprovalPendingReplyPayload({
approvalId: request.id,
approvalSlug: request.id.slice(0, 8),
approvalCommandId: request.id,
command: resolveExecApprovalCommandDisplay(request.request).commandText,
cwd: request.request.cwd ?? undefined,
host: request.request.host === "node" ? "node" : "gateway",
nodeId: request.request.nodeId ?? undefined,
expiresAtMs: request.expiresAtMs,
nowMs,
});
const buttons = buildTelegramExecApprovalButtons(request.id);
if (!buttons) {
return payload;
}
return {
...payload,
channelData: {
...payload.channelData,
telegram: {
buttons,
},
},
};
},
shouldSuppressForwardingFallback: (params) =>
shouldSuppressTelegramExecApprovalForwardingFallback(params),
buildPendingPayload: ({ request, nowMs }) =>
buildTelegramExecApprovalPendingPayload({ request, nowMs }),
beforeDeliverPending: async ({ cfg, target, payload }) => {
const hasExecApprovalData =
payload.channelData &&

View File

@@ -0,0 +1,55 @@
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import type { ExecApprovalRequest } from "openclaw/plugin-sdk/infra-runtime";
import {
buildExecApprovalPendingReplyPayload,
resolveExecApprovalCommandDisplay,
} from "openclaw/plugin-sdk/infra-runtime";
import { normalizeMessageChannel } from "openclaw/plugin-sdk/routing";
import { buildTelegramExecApprovalButtons } from "./approval-buttons.js";
import { isTelegramExecApprovalClientEnabled } from "./exec-approvals.js";
export function shouldSuppressTelegramExecApprovalForwardingFallback(params: {
cfg: OpenClawConfig;
target: { channel: string; accountId?: string | null };
request: ExecApprovalRequest;
}): boolean {
const channel = normalizeMessageChannel(params.target.channel) ?? params.target.channel;
if (channel !== "telegram") {
return false;
}
const requestChannel = normalizeMessageChannel(params.request.request.turnSourceChannel ?? "");
if (requestChannel !== "telegram") {
return false;
}
const accountId =
params.target.accountId?.trim() || params.request.request.turnSourceAccountId?.trim();
return isTelegramExecApprovalClientEnabled({ cfg: params.cfg, accountId });
}
export function buildTelegramExecApprovalPendingPayload(params: {
request: ExecApprovalRequest;
nowMs: number;
}) {
const payload = buildExecApprovalPendingReplyPayload({
approvalId: params.request.id,
approvalSlug: params.request.id.slice(0, 8),
approvalCommandId: params.request.id,
command: resolveExecApprovalCommandDisplay(params.request.request).commandText,
cwd: params.request.request.cwd ?? undefined,
host: params.request.request.host === "node" ? "node" : "gateway",
nodeId: params.request.request.nodeId ?? undefined,
expiresAtMs: params.request.expiresAtMs,
nowMs: params.nowMs,
});
const buttons = buildTelegramExecApprovalButtons(params.request.id);
if (!buttons) {
return payload;
}
return {
...payload,
channelData: {
...payload.channelData,
telegram: { buttons },
},
};
}

View File

@@ -1,10 +1,11 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { isDiscordExecApprovalClientEnabled } from "../../extensions/discord/src/exec-approvals.js";
import { buildTelegramExecApprovalButtons } from "../../extensions/telegram/src/approval-buttons.js";
import { isTelegramExecApprovalClientEnabled } from "../../extensions/telegram/src/exec-approvals.js";
import type { ChannelPlugin } from "../channels/plugins/types.js";
import type { OpenClawConfig } from "../config/config.js";
import { buildExecApprovalPendingReplyPayload } from "../infra/exec-approval-reply.js";
import {
buildTelegramExecApprovalPendingPayload,
shouldSuppressTelegramExecApprovalForwardingFallback,
} from "../plugin-sdk/telegram.js";
import { setActivePluginRegistry } from "../plugins/runtime.js";
import { createChannelTestPluginBase, createTestRegistry } from "../test-utils/channel-plugins.js";
import { createExecApprovalForwarder } from "./exec-approval-forwarder.js";
@@ -32,37 +33,10 @@ const telegramApprovalPlugin: Pick<
> = {
...createChannelTestPluginBase({ id: "telegram" }),
execApprovals: {
shouldSuppressForwardingFallback: ({ cfg, target, request }) => {
if (target.channel !== "telegram" || request.request.turnSourceChannel !== "telegram") {
return false;
}
const accountId = target.accountId?.trim() || request.request.turnSourceAccountId?.trim();
return isTelegramExecApprovalClientEnabled({ cfg, accountId });
},
buildPendingPayload: ({ request, nowMs }) => {
const payload = buildExecApprovalPendingReplyPayload({
approvalId: request.id,
approvalSlug: request.id.slice(0, 8),
approvalCommandId: request.id,
command: request.request.command,
cwd: request.request.cwd ?? undefined,
host: request.request.host === "node" ? "node" : "gateway",
nodeId: request.request.nodeId ?? undefined,
expiresAtMs: request.expiresAtMs,
nowMs,
});
const buttons = buildTelegramExecApprovalButtons(request.id);
if (!buttons) {
return payload;
}
return {
...payload,
channelData: {
...payload.channelData,
telegram: { buttons },
},
};
},
shouldSuppressForwardingFallback: (params) =>
shouldSuppressTelegramExecApprovalForwardingFallback(params),
buildPendingPayload: ({ request, nowMs }) =>
buildTelegramExecApprovalPendingPayload({ request, nowMs }),
},
};
const discordApprovalPlugin: Pick<

View File

@@ -2,10 +2,10 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { parseSlackTarget } from "../../../extensions/slack/src/targets.js";
import { parseTelegramTarget } from "../../../extensions/telegram/src/targets.js";
import type { ChannelThreadingToolContext } from "../../channels/plugins/types.js";
import type { OpenClawConfig } from "../../config/config.js";
import { resolveSlackAutoThreadId } from "../../plugin-sdk/slack.js";
import { resolveTelegramAutoThreadId } from "../../plugin-sdk/telegram.js";
import {
hydrateAttachmentParamsForAction,
normalizeSandboxMediaList,
@@ -27,51 +27,6 @@ function createToolContext(
};
}
function resolveSlackAutoThreadId(params: {
to: string;
toolContext?: {
currentChannelId?: string;
currentThreadTs?: string;
replyToMode?: "off" | "first" | "all";
hasRepliedRef?: { value: boolean };
};
}): string | undefined {
const context = params.toolContext;
if (!context?.currentThreadTs || !context.currentChannelId) {
return undefined;
}
if (context.replyToMode !== "all" && context.replyToMode !== "first") {
return undefined;
}
const parsedTarget = parseSlackTarget(params.to, { defaultKind: "channel" });
if (!parsedTarget || parsedTarget.kind !== "channel") {
return undefined;
}
if (parsedTarget.id.toLowerCase() !== context.currentChannelId.toLowerCase()) {
return undefined;
}
if (context.replyToMode === "first" && context.hasRepliedRef?.value) {
return undefined;
}
return context.currentThreadTs;
}
function resolveTelegramAutoThreadId(params: {
to: string;
toolContext?: { currentThreadTs?: string; currentChannelId?: string };
}): string | undefined {
const context = params.toolContext;
if (!context?.currentThreadTs || !context.currentChannelId) {
return undefined;
}
const parsedTo = parseTelegramTarget(params.to);
const parsedChannel = parseTelegramTarget(context.currentChannelId);
if (parsedTo.chatId.toLowerCase() !== parsedChannel.chatId.toLowerCase()) {
return undefined;
}
return context.currentThreadTs;
}
describe("message action threading helpers", () => {
it("resolves Slack auto-thread ids only for matching active channels", () => {
expect(

View File

@@ -58,6 +58,7 @@ export { inspectSlackAccount } from "../../extensions/slack/api.js";
export { parseSlackTarget, resolveSlackChannelId } from "./slack-targets.js";
export { extractSlackToolSend, listSlackMessageActions } from "../../extensions/slack/api.js";
export { buildSlackThreadingToolContext } from "../../extensions/slack/api.js";
export { resolveSlackAutoThreadId } from "../../extensions/slack/src/action-threading.js";
export { parseSlackBlocksInput } from "../../extensions/slack/api.js";
export { handleSlackHttpRequest } from "../../extensions/slack/api.js";
export { createSlackWebClient } from "../../extensions/slack/src/client.js";

View File

@@ -129,3 +129,8 @@ export {
isTelegramExecApprovalApprover,
isTelegramExecApprovalClientEnabled,
} from "../../../extensions/telegram/api.js";
export { resolveTelegramAutoThreadId } from "../../../extensions/telegram/src/action-threading.js";
export {
buildTelegramExecApprovalPendingPayload,
shouldSuppressTelegramExecApprovalForwardingFallback,
} from "../../../extensions/telegram/src/exec-approval-forwarding.js";