mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-20 21:23:23 +00:00
refactor: share slack and telegram action helpers
This commit is contained in:
30
extensions/slack/src/action-threading.ts
Normal file
30
extensions/slack/src/action-threading.ts
Normal 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;
|
||||
}
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
17
extensions/telegram/src/action-threading.ts
Normal file
17
extensions/telegram/src/action-threading.ts
Normal 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;
|
||||
}
|
||||
@@ -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 &&
|
||||
|
||||
55
extensions/telegram/src/exec-approval-forwarding.ts
Normal file
55
extensions/telegram/src/exec-approval-forwarding.ts
Normal 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 },
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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<
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user