mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-21 16:41:56 +00:00
refactor(outbound): dedupe poll threading + tighten duration semantics
This commit is contained in:
@@ -21,6 +21,7 @@ import {
|
||||
resolveChannelId,
|
||||
sendDiscordMedia,
|
||||
sendDiscordText,
|
||||
SUPPRESS_NOTIFICATIONS_FLAG,
|
||||
} from "./send.shared.js";
|
||||
import {
|
||||
ensureOggOpus,
|
||||
@@ -273,9 +274,11 @@ export async function sendPollDiscord(
|
||||
const recipient = await parseAndResolveRecipient(to, opts.accountId);
|
||||
const { channelId } = await resolveChannelId(rest, recipient, request);
|
||||
const content = opts.content?.trim();
|
||||
if (poll.durationSeconds !== undefined) {
|
||||
throw new Error("Discord polls do not support durationSeconds; use durationHours");
|
||||
}
|
||||
const payload = normalizeDiscordPollInput(poll);
|
||||
// Discord message flag for silent/suppress notifications (matches send.shared.ts)
|
||||
const flags = opts.silent ? 1 << 12 : undefined;
|
||||
const flags = opts.silent ? SUPPRESS_NOTIFICATIONS_FLAG : undefined;
|
||||
const res = (await request(
|
||||
() =>
|
||||
rest.post(Routes.channelMessages(channelId), {
|
||||
|
||||
@@ -232,7 +232,7 @@ async function resolveChannelId(
|
||||
}
|
||||
|
||||
// Discord message flag for silent/suppress notifications
|
||||
const SUPPRESS_NOTIFICATIONS_FLAG = 1 << 12;
|
||||
export const SUPPRESS_NOTIFICATIONS_FLAG = 1 << 12;
|
||||
|
||||
export function buildDiscordTextChunks(
|
||||
text: string,
|
||||
|
||||
@@ -36,7 +36,7 @@ export const PollParamsSchema = Type.Object(
|
||||
options: Type.Array(NonEmptyString, { minItems: 2, maxItems: 12 }),
|
||||
maxSelections: Type.Optional(Type.Integer({ minimum: 1, maximum: 12 })),
|
||||
/** Poll duration in seconds (channel-specific limits may apply). */
|
||||
durationSeconds: Type.Optional(Type.Integer({ minimum: 1, maximum: 600 })),
|
||||
durationSeconds: Type.Optional(Type.Integer({ minimum: 1, maximum: 604_800 })),
|
||||
durationHours: Type.Optional(Type.Integer({ minimum: 1 })),
|
||||
/** Send silently (no notification) where supported. */
|
||||
silent: Type.Optional(Type.Boolean()),
|
||||
|
||||
@@ -303,6 +303,25 @@ export const sendHandlers: GatewayRequestHandlers = {
|
||||
return;
|
||||
}
|
||||
const channel = normalizedChannel ?? DEFAULT_CHAT_CHANNEL;
|
||||
if (typeof request.durationSeconds === "number" && channel !== "telegram") {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
"durationSeconds is only supported for Telegram polls",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (typeof request.isAnonymous === "boolean" && channel !== "telegram") {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.INVALID_REQUEST, "isAnonymous is only supported for Telegram polls"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const poll = {
|
||||
question: request.question,
|
||||
options: request.options,
|
||||
|
||||
@@ -59,6 +59,33 @@ export type MessageActionRunnerGateway = {
|
||||
mode: GatewayClientMode;
|
||||
};
|
||||
|
||||
function resolveAndApplyOutboundThreadId(
|
||||
params: Record<string, unknown>,
|
||||
ctx: {
|
||||
channel: ChannelId;
|
||||
to: string;
|
||||
toolContext?: ChannelThreadingToolContext;
|
||||
allowSlackAutoThread: boolean;
|
||||
},
|
||||
): string | undefined {
|
||||
const threadId = readStringParam(params, "threadId");
|
||||
const slackAutoThreadId =
|
||||
ctx.allowSlackAutoThread && ctx.channel === "slack" && !threadId
|
||||
? resolveSlackAutoThreadId({ to: ctx.to, toolContext: ctx.toolContext })
|
||||
: undefined;
|
||||
const telegramAutoThreadId =
|
||||
ctx.channel === "telegram" && !threadId
|
||||
? resolveTelegramAutoThreadId({ to: ctx.to, toolContext: ctx.toolContext })
|
||||
: undefined;
|
||||
const resolved = threadId ?? slackAutoThreadId ?? telegramAutoThreadId;
|
||||
// Write auto-resolved threadId back into params so downstream dispatch
|
||||
// (plugin `readStringParam(params, "threadId")`) picks it up.
|
||||
if (resolved && !params.threadId) {
|
||||
params.threadId = resolved;
|
||||
}
|
||||
return resolved ?? undefined;
|
||||
}
|
||||
|
||||
export type RunMessageActionParams = {
|
||||
cfg: OpenClawConfig;
|
||||
action: ChannelMessageActionName;
|
||||
@@ -469,23 +496,12 @@ async function handleSendAction(ctx: ResolvedActionContext): Promise<MessageActi
|
||||
const silent = readBooleanParam(params, "silent");
|
||||
|
||||
const replyToId = readStringParam(params, "replyTo");
|
||||
const threadId = readStringParam(params, "threadId");
|
||||
// Slack auto-threading can inject threadTs without explicit params; mirror to that session key.
|
||||
const slackAutoThreadId =
|
||||
channel === "slack" && !replyToId && !threadId
|
||||
? resolveSlackAutoThreadId({ to, toolContext: input.toolContext })
|
||||
: undefined;
|
||||
// Telegram forum topic auto-threading: inject threadId so media/buttons land in the correct topic.
|
||||
const telegramAutoThreadId =
|
||||
channel === "telegram" && !threadId
|
||||
? resolveTelegramAutoThreadId({ to, toolContext: input.toolContext })
|
||||
: undefined;
|
||||
const resolvedThreadId = threadId ?? slackAutoThreadId ?? telegramAutoThreadId;
|
||||
// Write auto-resolved threadId back into params so downstream dispatch
|
||||
// (plugin `readStringParam(params, "threadId")`) picks it up.
|
||||
if (resolvedThreadId && !params.threadId) {
|
||||
params.threadId = resolvedThreadId;
|
||||
}
|
||||
const resolvedThreadId = resolveAndApplyOutboundThreadId(params, {
|
||||
channel,
|
||||
to,
|
||||
toolContext: input.toolContext,
|
||||
allowSlackAutoThread: channel === "slack" && !replyToId,
|
||||
});
|
||||
const outboundRoute =
|
||||
agentId && !dryRun
|
||||
? await resolveOutboundSessionRoute({
|
||||
@@ -584,19 +600,19 @@ async function handlePollAction(ctx: ResolvedActionContext): Promise<MessageActi
|
||||
});
|
||||
const maxSelections = allowMultiselect ? Math.max(2, options.length) : 1;
|
||||
|
||||
const threadId = readStringParam(params, "threadId");
|
||||
const slackAutoThreadId =
|
||||
channel === "slack" && !threadId
|
||||
? resolveSlackAutoThreadId({ to, toolContext: input.toolContext })
|
||||
: undefined;
|
||||
const telegramAutoThreadId =
|
||||
channel === "telegram" && !threadId
|
||||
? resolveTelegramAutoThreadId({ to, toolContext: input.toolContext })
|
||||
: undefined;
|
||||
const resolvedThreadId = threadId ?? slackAutoThreadId ?? telegramAutoThreadId;
|
||||
if (resolvedThreadId && !params.threadId) {
|
||||
params.threadId = resolvedThreadId;
|
||||
if (durationSeconds !== undefined && channel !== "telegram") {
|
||||
throw new Error("pollDurationSeconds is only supported for Telegram polls");
|
||||
}
|
||||
if (isAnonymous !== undefined && channel !== "telegram") {
|
||||
throw new Error("pollAnonymous/pollPublic are only supported for Telegram polls");
|
||||
}
|
||||
|
||||
const resolvedThreadId = resolveAndApplyOutboundThreadId(params, {
|
||||
channel,
|
||||
to,
|
||||
toolContext: input.toolContext,
|
||||
allowSlackAutoThread: channel === "slack",
|
||||
});
|
||||
|
||||
const base = typeof params.message === "string" ? params.message : "";
|
||||
await maybeApplyCrossContextMarker({
|
||||
|
||||
@@ -29,4 +29,15 @@ describe("polls", () => {
|
||||
expect(normalizePollDurationHours(999, { defaultHours: 24, maxHours: 48 })).toBe(48);
|
||||
expect(normalizePollDurationHours(1, { defaultHours: 24, maxHours: 48 })).toBe(1);
|
||||
});
|
||||
|
||||
it("rejects both durationSeconds and durationHours", () => {
|
||||
expect(() =>
|
||||
normalizePollInput({
|
||||
question: "Q",
|
||||
options: ["A", "B"],
|
||||
durationSeconds: 60,
|
||||
durationHours: 1,
|
||||
}),
|
||||
).toThrow(/mutually exclusive/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -71,6 +71,9 @@ export function normalizePollInput(
|
||||
if (durationHours !== undefined && durationHours < 1) {
|
||||
throw new Error("durationHours must be at least 1");
|
||||
}
|
||||
if (durationSeconds !== undefined && durationHours !== undefined) {
|
||||
throw new Error("durationSeconds and durationHours are mutually exclusive");
|
||||
}
|
||||
return {
|
||||
question,
|
||||
options: cleaned,
|
||||
|
||||
Reference in New Issue
Block a user