mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 01:13:29 +00:00
refactor(outbound): dedupe poll threading + tighten duration semantics
This commit is contained in:
@@ -21,6 +21,7 @@ import {
|
|||||||
resolveChannelId,
|
resolveChannelId,
|
||||||
sendDiscordMedia,
|
sendDiscordMedia,
|
||||||
sendDiscordText,
|
sendDiscordText,
|
||||||
|
SUPPRESS_NOTIFICATIONS_FLAG,
|
||||||
} from "./send.shared.js";
|
} from "./send.shared.js";
|
||||||
import {
|
import {
|
||||||
ensureOggOpus,
|
ensureOggOpus,
|
||||||
@@ -273,9 +274,11 @@ export async function sendPollDiscord(
|
|||||||
const recipient = await parseAndResolveRecipient(to, opts.accountId);
|
const recipient = await parseAndResolveRecipient(to, opts.accountId);
|
||||||
const { channelId } = await resolveChannelId(rest, recipient, request);
|
const { channelId } = await resolveChannelId(rest, recipient, request);
|
||||||
const content = opts.content?.trim();
|
const content = opts.content?.trim();
|
||||||
|
if (poll.durationSeconds !== undefined) {
|
||||||
|
throw new Error("Discord polls do not support durationSeconds; use durationHours");
|
||||||
|
}
|
||||||
const payload = normalizeDiscordPollInput(poll);
|
const payload = normalizeDiscordPollInput(poll);
|
||||||
// Discord message flag for silent/suppress notifications (matches send.shared.ts)
|
const flags = opts.silent ? SUPPRESS_NOTIFICATIONS_FLAG : undefined;
|
||||||
const flags = opts.silent ? 1 << 12 : undefined;
|
|
||||||
const res = (await request(
|
const res = (await request(
|
||||||
() =>
|
() =>
|
||||||
rest.post(Routes.channelMessages(channelId), {
|
rest.post(Routes.channelMessages(channelId), {
|
||||||
|
|||||||
@@ -232,7 +232,7 @@ async function resolveChannelId(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Discord message flag for silent/suppress notifications
|
// Discord message flag for silent/suppress notifications
|
||||||
const SUPPRESS_NOTIFICATIONS_FLAG = 1 << 12;
|
export const SUPPRESS_NOTIFICATIONS_FLAG = 1 << 12;
|
||||||
|
|
||||||
export function buildDiscordTextChunks(
|
export function buildDiscordTextChunks(
|
||||||
text: string,
|
text: string,
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export const PollParamsSchema = Type.Object(
|
|||||||
options: Type.Array(NonEmptyString, { minItems: 2, maxItems: 12 }),
|
options: Type.Array(NonEmptyString, { minItems: 2, maxItems: 12 }),
|
||||||
maxSelections: Type.Optional(Type.Integer({ minimum: 1, maximum: 12 })),
|
maxSelections: Type.Optional(Type.Integer({ minimum: 1, maximum: 12 })),
|
||||||
/** Poll duration in seconds (channel-specific limits may apply). */
|
/** 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 })),
|
durationHours: Type.Optional(Type.Integer({ minimum: 1 })),
|
||||||
/** Send silently (no notification) where supported. */
|
/** Send silently (no notification) where supported. */
|
||||||
silent: Type.Optional(Type.Boolean()),
|
silent: Type.Optional(Type.Boolean()),
|
||||||
|
|||||||
@@ -303,6 +303,25 @@ export const sendHandlers: GatewayRequestHandlers = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const channel = normalizedChannel ?? DEFAULT_CHAT_CHANNEL;
|
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 = {
|
const poll = {
|
||||||
question: request.question,
|
question: request.question,
|
||||||
options: request.options,
|
options: request.options,
|
||||||
|
|||||||
@@ -59,6 +59,33 @@ export type MessageActionRunnerGateway = {
|
|||||||
mode: GatewayClientMode;
|
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 = {
|
export type RunMessageActionParams = {
|
||||||
cfg: OpenClawConfig;
|
cfg: OpenClawConfig;
|
||||||
action: ChannelMessageActionName;
|
action: ChannelMessageActionName;
|
||||||
@@ -469,23 +496,12 @@ async function handleSendAction(ctx: ResolvedActionContext): Promise<MessageActi
|
|||||||
const silent = readBooleanParam(params, "silent");
|
const silent = readBooleanParam(params, "silent");
|
||||||
|
|
||||||
const replyToId = readStringParam(params, "replyTo");
|
const replyToId = readStringParam(params, "replyTo");
|
||||||
const threadId = readStringParam(params, "threadId");
|
const resolvedThreadId = resolveAndApplyOutboundThreadId(params, {
|
||||||
// Slack auto-threading can inject threadTs without explicit params; mirror to that session key.
|
channel,
|
||||||
const slackAutoThreadId =
|
to,
|
||||||
channel === "slack" && !replyToId && !threadId
|
toolContext: input.toolContext,
|
||||||
? resolveSlackAutoThreadId({ to, toolContext: input.toolContext })
|
allowSlackAutoThread: channel === "slack" && !replyToId,
|
||||||
: 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 outboundRoute =
|
const outboundRoute =
|
||||||
agentId && !dryRun
|
agentId && !dryRun
|
||||||
? await resolveOutboundSessionRoute({
|
? await resolveOutboundSessionRoute({
|
||||||
@@ -584,19 +600,19 @@ async function handlePollAction(ctx: ResolvedActionContext): Promise<MessageActi
|
|||||||
});
|
});
|
||||||
const maxSelections = allowMultiselect ? Math.max(2, options.length) : 1;
|
const maxSelections = allowMultiselect ? Math.max(2, options.length) : 1;
|
||||||
|
|
||||||
const threadId = readStringParam(params, "threadId");
|
if (durationSeconds !== undefined && channel !== "telegram") {
|
||||||
const slackAutoThreadId =
|
throw new Error("pollDurationSeconds is only supported for Telegram polls");
|
||||||
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 (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 : "";
|
const base = typeof params.message === "string" ? params.message : "";
|
||||||
await maybeApplyCrossContextMarker({
|
await maybeApplyCrossContextMarker({
|
||||||
|
|||||||
@@ -29,4 +29,15 @@ describe("polls", () => {
|
|||||||
expect(normalizePollDurationHours(999, { defaultHours: 24, maxHours: 48 })).toBe(48);
|
expect(normalizePollDurationHours(999, { defaultHours: 24, maxHours: 48 })).toBe(48);
|
||||||
expect(normalizePollDurationHours(1, { defaultHours: 24, maxHours: 48 })).toBe(1);
|
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) {
|
if (durationHours !== undefined && durationHours < 1) {
|
||||||
throw new Error("durationHours must be at least 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 {
|
return {
|
||||||
question,
|
question,
|
||||||
options: cleaned,
|
options: cleaned,
|
||||||
|
|||||||
Reference in New Issue
Block a user