mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 04:10:46 +00:00
Add /btw side questions (#45444)
* feat(agent): add /btw side questions * fix(agent): gate and log /btw reviews * feat(btw): isolate side-question delivery * test(reply): update route reply runtime mocks * fix(btw): complete side-result delivery across clients * fix(gateway): handle streamed btw side results * fix(telegram): unblock btw side questions * fix(reply): make external btw replies explicit * fix(chat): keep btw side results ephemeral in internal history * fix(btw): address remaining review feedback * fix(chat): preserve btw history on mobile refresh * fix(acp): keep btw replies out of prompt history * refactor(btw): narrow side questions to live channels * fix(btw): preserve channel typing indicators * fix(btw): keep side questions isolated in chat * fix(outbound): restore typed channel send deps * fix(btw): avoid blocking replies on transcript persistence * fix(btw): keep side questions fast * docs(commands): document btw slash command * docs(changelog): add btw side questions entry * test(outbound): align session transcript mocks
This commit is contained in:
@@ -196,6 +196,14 @@ function buildChatCommands(): ChatCommandDefinition[] {
|
||||
acceptsArgs: true,
|
||||
category: "status",
|
||||
}),
|
||||
defineChatCommand({
|
||||
key: "btw",
|
||||
nativeName: "btw",
|
||||
description: "Ask a side question without changing future session context.",
|
||||
textAlias: "/btw",
|
||||
acceptsArgs: true,
|
||||
category: "tools",
|
||||
}),
|
||||
defineChatCommand({
|
||||
key: "export-session",
|
||||
nativeName: "export-session",
|
||||
|
||||
26
src/auto-reply/reply/btw-command.ts
Normal file
26
src/auto-reply/reply/btw-command.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { normalizeCommandBody, type CommandNormalizeOptions } from "../commands-registry.js";
|
||||
|
||||
const BTW_COMMAND_RE = /^\/btw(?::|\s|$)/i;
|
||||
|
||||
export function isBtwRequestText(text?: string, options?: CommandNormalizeOptions): boolean {
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
const normalized = normalizeCommandBody(text, options).trim();
|
||||
return BTW_COMMAND_RE.test(normalized);
|
||||
}
|
||||
|
||||
export function extractBtwQuestion(
|
||||
text?: string,
|
||||
options?: CommandNormalizeOptions,
|
||||
): string | null {
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
const normalized = normalizeCommandBody(text, options).trim();
|
||||
const match = normalized.match(/^\/btw(?:\s+(.*))?$/i);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
return match[1]?.trim() ?? "";
|
||||
}
|
||||
131
src/auto-reply/reply/commands-btw.test.ts
Normal file
131
src/auto-reply/reply/commands-btw.test.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { buildCommandTestParams } from "./commands.test-harness.js";
|
||||
import { createMockTypingController } from "./test-helpers.js";
|
||||
|
||||
const runBtwSideQuestionMock = vi.fn();
|
||||
|
||||
vi.mock("../../agents/btw.js", () => ({
|
||||
runBtwSideQuestion: (...args: unknown[]) => runBtwSideQuestionMock(...args),
|
||||
}));
|
||||
|
||||
const { handleBtwCommand } = await import("./commands-btw.js");
|
||||
|
||||
function buildParams(commandBody: string) {
|
||||
const cfg = {
|
||||
commands: { text: true },
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
} as OpenClawConfig;
|
||||
return buildCommandTestParams(commandBody, cfg, undefined, { workspaceDir: "/tmp/workspace" });
|
||||
}
|
||||
|
||||
describe("handleBtwCommand", () => {
|
||||
beforeEach(() => {
|
||||
runBtwSideQuestionMock.mockReset();
|
||||
});
|
||||
|
||||
it("returns usage when the side question is missing", async () => {
|
||||
const result = await handleBtwCommand(buildParams("/btw"), true);
|
||||
|
||||
expect(result).toEqual({
|
||||
shouldContinue: false,
|
||||
reply: { text: "Usage: /btw <side question>" },
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores /btw when text commands are disabled", async () => {
|
||||
const result = await handleBtwCommand(buildParams("/btw what changed?"), false);
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(runBtwSideQuestionMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("ignores /btw from unauthorized senders", async () => {
|
||||
const params = buildParams("/btw what changed?");
|
||||
params.command.isAuthorizedSender = false;
|
||||
|
||||
const result = await handleBtwCommand(params, true);
|
||||
|
||||
expect(result).toEqual({ shouldContinue: false });
|
||||
expect(runBtwSideQuestionMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("requires an active session context", async () => {
|
||||
const params = buildParams("/btw what changed?");
|
||||
params.sessionEntry = undefined;
|
||||
|
||||
const result = await handleBtwCommand(params, true);
|
||||
|
||||
expect(result).toEqual({
|
||||
shouldContinue: false,
|
||||
reply: { text: "⚠️ /btw requires an active session with existing context." },
|
||||
});
|
||||
});
|
||||
|
||||
it("still delegates while the session is actively running", async () => {
|
||||
const params = buildParams("/btw what changed?");
|
||||
params.agentDir = "/tmp/agent";
|
||||
params.sessionEntry = {
|
||||
sessionId: "session-1",
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
runBtwSideQuestionMock.mockResolvedValue({ text: "snapshot answer" });
|
||||
|
||||
const result = await handleBtwCommand(params, true);
|
||||
|
||||
expect(runBtwSideQuestionMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
question: "what changed?",
|
||||
sessionEntry: params.sessionEntry,
|
||||
resolvedThinkLevel: "off",
|
||||
resolvedReasoningLevel: "off",
|
||||
}),
|
||||
);
|
||||
expect(result).toEqual({
|
||||
shouldContinue: false,
|
||||
reply: { text: "snapshot answer", btw: { question: "what changed?" } },
|
||||
});
|
||||
});
|
||||
|
||||
it("starts the typing keepalive while the side question runs", async () => {
|
||||
const params = buildParams("/btw what changed?");
|
||||
const typing = createMockTypingController();
|
||||
params.typing = typing;
|
||||
params.agentDir = "/tmp/agent";
|
||||
params.sessionEntry = {
|
||||
sessionId: "session-1",
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
runBtwSideQuestionMock.mockResolvedValue({ text: "snapshot answer" });
|
||||
|
||||
await handleBtwCommand(params, true);
|
||||
|
||||
expect(typing.startTypingLoop).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("delegates to the side-question runner", async () => {
|
||||
const params = buildParams("/btw what changed?");
|
||||
params.agentDir = "/tmp/agent";
|
||||
params.sessionEntry = {
|
||||
sessionId: "session-1",
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
runBtwSideQuestionMock.mockResolvedValue({ text: "nothing important" });
|
||||
|
||||
const result = await handleBtwCommand(params, true);
|
||||
|
||||
expect(runBtwSideQuestionMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
question: "what changed?",
|
||||
agentDir: "/tmp/agent",
|
||||
sessionEntry: params.sessionEntry,
|
||||
resolvedThinkLevel: "off",
|
||||
resolvedReasoningLevel: "off",
|
||||
}),
|
||||
);
|
||||
expect(result).toEqual({
|
||||
shouldContinue: false,
|
||||
reply: { text: "nothing important", btw: { question: "what changed?" } },
|
||||
});
|
||||
});
|
||||
});
|
||||
80
src/auto-reply/reply/commands-btw.ts
Normal file
80
src/auto-reply/reply/commands-btw.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { runBtwSideQuestion } from "../../agents/btw.js";
|
||||
import { extractBtwQuestion } from "./btw-command.js";
|
||||
import { rejectUnauthorizedCommand } from "./command-gates.js";
|
||||
import type { CommandHandler } from "./commands-types.js";
|
||||
|
||||
const BTW_USAGE = "Usage: /btw <side question>";
|
||||
|
||||
export const handleBtwCommand: CommandHandler = async (params, allowTextCommands) => {
|
||||
if (!allowTextCommands) {
|
||||
return null;
|
||||
}
|
||||
const question = extractBtwQuestion(params.command.commandBodyNormalized);
|
||||
if (question === null) {
|
||||
return null;
|
||||
}
|
||||
const unauthorized = rejectUnauthorizedCommand(params, "/btw");
|
||||
if (unauthorized) {
|
||||
return unauthorized;
|
||||
}
|
||||
|
||||
if (!question) {
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: { text: BTW_USAGE },
|
||||
};
|
||||
}
|
||||
|
||||
if (!params.sessionEntry?.sessionId) {
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: { text: "⚠️ /btw requires an active session with existing context." },
|
||||
};
|
||||
}
|
||||
|
||||
if (!params.agentDir) {
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: {
|
||||
text: "⚠️ /btw is unavailable because the active agent directory could not be resolved.",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
await params.typing?.startTypingLoop();
|
||||
const reply = await runBtwSideQuestion({
|
||||
cfg: params.cfg,
|
||||
agentDir: params.agentDir,
|
||||
provider: params.provider,
|
||||
model: params.model,
|
||||
question,
|
||||
sessionEntry: params.sessionEntry,
|
||||
sessionStore: params.sessionStore,
|
||||
sessionKey: params.sessionKey,
|
||||
storePath: params.storePath,
|
||||
// BTW is intentionally a quick side question, so do not inherit slower
|
||||
// session-level think/reasoning settings from the main run.
|
||||
resolvedThinkLevel: "off",
|
||||
resolvedReasoningLevel: "off",
|
||||
blockReplyChunking: params.blockReplyChunking,
|
||||
resolvedBlockStreamingBreak: params.resolvedBlockStreamingBreak,
|
||||
opts: params.opts,
|
||||
isNewSession: false,
|
||||
});
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: reply ? { ...reply, btw: { question } } : reply,
|
||||
};
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message.trim() : "";
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: {
|
||||
text: `⚠️ /btw failed${message ? `: ${message}` : "."}`,
|
||||
btw: { question },
|
||||
isError: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -11,6 +11,7 @@ import { resolveBoundAcpThreadSessionKey } from "./commands-acp/targets.js";
|
||||
import { handleAllowlistCommand } from "./commands-allowlist.js";
|
||||
import { handleApproveCommand } from "./commands-approve.js";
|
||||
import { handleBashCommand } from "./commands-bash.js";
|
||||
import { handleBtwCommand } from "./commands-btw.js";
|
||||
import { handleCompactCommand } from "./commands-compact.js";
|
||||
import { handleConfigCommand, handleDebugCommand } from "./commands-config.js";
|
||||
import {
|
||||
@@ -174,6 +175,7 @@ export async function handleCommands(params: HandleCommandsParams): Promise<Comm
|
||||
HANDLERS = [
|
||||
// Plugin commands are processed first, before built-in commands
|
||||
handlePluginCommand,
|
||||
handleBtwCommand,
|
||||
handleBashCommand,
|
||||
handleActivationCommand,
|
||||
handleSendPolicyCommand,
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import type { BlockReplyChunking } from "../../agents/pi-embedded-block-chunker.js";
|
||||
import type { SkillCommandSpec } from "../../agents/skills.js";
|
||||
import type { ChannelId } from "../../channels/plugins/types.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import type { SessionEntry, SessionScope } from "../../config/sessions.js";
|
||||
import type { MsgContext } from "../templating.js";
|
||||
import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "../thinking.js";
|
||||
import type { ReplyPayload } from "../types.js";
|
||||
import type { GetReplyOptions, ReplyPayload } from "../types.js";
|
||||
import type { InlineDirectives } from "./directive-handling.js";
|
||||
import type { TypingController } from "./typing.js";
|
||||
|
||||
export type CommandContext = {
|
||||
surface: string;
|
||||
@@ -44,17 +46,21 @@ export type HandleCommandsParams = {
|
||||
storePath?: string;
|
||||
sessionScope?: SessionScope;
|
||||
workspaceDir: string;
|
||||
opts?: GetReplyOptions;
|
||||
defaultGroupActivation: () => "always" | "mention";
|
||||
resolvedThinkLevel?: ThinkLevel;
|
||||
resolvedVerboseLevel: VerboseLevel;
|
||||
resolvedReasoningLevel: ReasoningLevel;
|
||||
resolvedElevatedLevel?: ElevatedLevel;
|
||||
blockReplyChunking?: BlockReplyChunking;
|
||||
resolvedBlockStreamingBreak?: "text_end" | "message_end";
|
||||
resolveDefaultThinkingLevel: () => Promise<ThinkLevel | undefined>;
|
||||
provider: string;
|
||||
model: string;
|
||||
contextTokens: number;
|
||||
isGroup: boolean;
|
||||
skillCommands?: SkillCommandSpec[];
|
||||
typing?: TypingController;
|
||||
};
|
||||
|
||||
export type CommandHandlerResult = {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { collectTextContentBlocks } from "../../agents/content-blocks.js";
|
||||
import { createOpenClawTools } from "../../agents/openclaw-tools.js";
|
||||
import type { BlockReplyChunking } from "../../agents/pi-embedded-block-chunker.js";
|
||||
import type { SkillCommandSpec } from "../../agents/skills.js";
|
||||
import { applyOwnerOnlyToolPolicy } from "../../agents/tool-policy.js";
|
||||
import { getChannelDock } from "../../channels/dock.js";
|
||||
@@ -37,6 +38,7 @@ function getBuiltinSlashCommands(): Set<string> {
|
||||
return builtinSlashCommands;
|
||||
}
|
||||
builtinSlashCommands = listReservedChatSlashCommandNames([
|
||||
"btw",
|
||||
"think",
|
||||
"verbose",
|
||||
"reasoning",
|
||||
@@ -113,6 +115,8 @@ export async function handleInlineActions(params: {
|
||||
resolvedVerboseLevel: VerboseLevel | undefined;
|
||||
resolvedReasoningLevel: ReasoningLevel;
|
||||
resolvedElevatedLevel: ElevatedLevel;
|
||||
blockReplyChunking?: BlockReplyChunking;
|
||||
resolvedBlockStreamingBreak?: "text_end" | "message_end";
|
||||
resolveDefaultThinkingLevel: Awaited<
|
||||
ReturnType<typeof createModelSelectionState>
|
||||
>["resolveDefaultThinkingLevel"];
|
||||
@@ -152,6 +156,8 @@ export async function handleInlineActions(params: {
|
||||
resolvedVerboseLevel,
|
||||
resolvedReasoningLevel,
|
||||
resolvedElevatedLevel,
|
||||
blockReplyChunking,
|
||||
resolvedBlockStreamingBreak,
|
||||
resolveDefaultThinkingLevel,
|
||||
provider,
|
||||
model,
|
||||
@@ -357,17 +363,21 @@ export async function handleInlineActions(params: {
|
||||
storePath,
|
||||
sessionScope,
|
||||
workspaceDir,
|
||||
opts,
|
||||
defaultGroupActivation: defaultActivation,
|
||||
resolvedThinkLevel,
|
||||
resolvedVerboseLevel: resolvedVerboseLevel ?? "off",
|
||||
resolvedReasoningLevel,
|
||||
resolvedElevatedLevel,
|
||||
blockReplyChunking,
|
||||
resolvedBlockStreamingBreak,
|
||||
resolveDefaultThinkingLevel,
|
||||
provider,
|
||||
model,
|
||||
contextTokens,
|
||||
isGroup,
|
||||
skillCommands,
|
||||
typing,
|
||||
});
|
||||
|
||||
if (inlineCommand) {
|
||||
|
||||
@@ -332,6 +332,8 @@ export async function getReplyFromConfig(
|
||||
resolvedVerboseLevel,
|
||||
resolvedReasoningLevel,
|
||||
resolvedElevatedLevel,
|
||||
blockReplyChunking,
|
||||
resolvedBlockStreamingBreak,
|
||||
resolveDefaultThinkingLevel: modelState.resolveDefaultThinkingLevel,
|
||||
provider,
|
||||
model,
|
||||
|
||||
@@ -10,6 +10,19 @@ import type { ReplyPayload } from "../types.js";
|
||||
import { extractReplyToTag } from "./reply-tags.js";
|
||||
import { createReplyToModeFilterForChannel } from "./reply-threading.js";
|
||||
|
||||
export function formatBtwTextForExternalDelivery(payload: ReplyPayload): string | undefined {
|
||||
const text = payload.text?.trim();
|
||||
if (!text) {
|
||||
return payload.text;
|
||||
}
|
||||
const question = payload.btw?.question?.trim();
|
||||
if (!question) {
|
||||
return payload.text;
|
||||
}
|
||||
const formatted = `BTW\nQuestion: ${question}\n\n${text}`;
|
||||
return text === formatted || text.startsWith("BTW\nQuestion:") ? text : formatted;
|
||||
}
|
||||
|
||||
function resolveReplyThreadingForPayload(params: {
|
||||
payload: ReplyPayload;
|
||||
implicitReplyToId?: string;
|
||||
|
||||
@@ -44,25 +44,33 @@ vi.mock("../../../extensions/slack/src/send.js", () => ({
|
||||
vi.mock("../../../extensions/telegram/src/send.js", () => ({
|
||||
sendMessageTelegram: mocks.sendMessageTelegram,
|
||||
}));
|
||||
vi.mock("../../../extensions/telegram/src/send.js", () => ({
|
||||
sendMessageTelegram: mocks.sendMessageTelegram,
|
||||
}));
|
||||
vi.mock("../../../extensions/whatsapp/src/send.js", () => ({
|
||||
sendMessageWhatsApp: mocks.sendMessageWhatsApp,
|
||||
sendPollWhatsApp: mocks.sendMessageWhatsApp,
|
||||
}));
|
||||
vi.mock("../../../extensions/discord/src/send.js", () => ({
|
||||
sendMessageDiscord: mocks.sendMessageDiscord,
|
||||
sendPollDiscord: mocks.sendMessageDiscord,
|
||||
sendWebhookMessageDiscord: vi.fn(),
|
||||
}));
|
||||
vi.mock("../../../extensions/mattermost/src/mattermost/send.js", () => ({
|
||||
sendMessageMattermost: mocks.sendMessageMattermost,
|
||||
}));
|
||||
vi.mock("../../infra/outbound/deliver.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../../infra/outbound/deliver.js")>(
|
||||
"../../infra/outbound/deliver.js",
|
||||
vi.mock("../../infra/outbound/deliver-runtime.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../../infra/outbound/deliver-runtime.js")>(
|
||||
"../../infra/outbound/deliver-runtime.js",
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
deliverOutboundPayloads: mocks.deliverOutboundPayloads,
|
||||
};
|
||||
});
|
||||
const actualDeliver = await vi.importActual<typeof import("../../infra/outbound/deliver.js")>(
|
||||
"../../infra/outbound/deliver.js",
|
||||
);
|
||||
const actualDeliver = await vi.importActual<
|
||||
typeof import("../../infra/outbound/deliver-runtime.js")
|
||||
>("../../infra/outbound/deliver-runtime.js");
|
||||
|
||||
const { routeReply } = await import("./route-reply.js");
|
||||
|
||||
@@ -294,6 +302,36 @@ describe("routeReply", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("formats BTW replies prominently on routed sends", async () => {
|
||||
mocks.sendMessageSlack.mockClear();
|
||||
await routeReply({
|
||||
payload: { text: "323", btw: { question: "what is 17 * 19?" } },
|
||||
channel: "slack",
|
||||
to: "channel:C123",
|
||||
cfg: {} as never,
|
||||
});
|
||||
expect(mocks.sendMessageSlack).toHaveBeenCalledWith(
|
||||
"channel:C123",
|
||||
"BTW\nQuestion: what is 17 * 19?\n\n323",
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("formats BTW replies prominently on routed discord sends", async () => {
|
||||
mocks.sendMessageDiscord.mockClear();
|
||||
await routeReply({
|
||||
payload: { text: "323", btw: { question: "what is 17 * 19?" } },
|
||||
channel: "discord",
|
||||
to: "channel:123456",
|
||||
cfg: {} as never,
|
||||
});
|
||||
expect(mocks.sendMessageDiscord).toHaveBeenCalledWith(
|
||||
"channel:123456",
|
||||
"BTW\nQuestion: what is 17 * 19?\n\n323",
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("passes replyToId to Telegram sends", async () => {
|
||||
mocks.sendMessageTelegram.mockClear();
|
||||
await routeReply({
|
||||
|
||||
@@ -18,7 +18,10 @@ import { INTERNAL_MESSAGE_CHANNEL, normalizeMessageChannel } from "../../utils/m
|
||||
import type { OriginatingChannelType } from "../templating.js";
|
||||
import type { ReplyPayload } from "../types.js";
|
||||
import { normalizeReplyPayload } from "./normalize-reply.js";
|
||||
import { shouldSuppressReasoningPayload } from "./reply-payloads.js";
|
||||
import {
|
||||
formatBtwTextForExternalDelivery,
|
||||
shouldSuppressReasoningPayload,
|
||||
} from "./reply-payloads.js";
|
||||
|
||||
let deliverRuntimePromise: Promise<
|
||||
typeof import("../../infra/outbound/deliver-runtime.js")
|
||||
@@ -102,24 +105,28 @@ export async function routeReply(params: RouteReplyParams): Promise<RouteReplyRe
|
||||
if (!normalized) {
|
||||
return { ok: true };
|
||||
}
|
||||
const externalPayload: ReplyPayload = {
|
||||
...normalized,
|
||||
text: formatBtwTextForExternalDelivery(normalized),
|
||||
};
|
||||
|
||||
let text = normalized.text ?? "";
|
||||
let mediaUrls = (normalized.mediaUrls?.filter(Boolean) ?? []).length
|
||||
? (normalized.mediaUrls?.filter(Boolean) as string[])
|
||||
: normalized.mediaUrl
|
||||
? [normalized.mediaUrl]
|
||||
let text = externalPayload.text ?? "";
|
||||
let mediaUrls = (externalPayload.mediaUrls?.filter(Boolean) ?? []).length
|
||||
? (externalPayload.mediaUrls?.filter(Boolean) as string[])
|
||||
: externalPayload.mediaUrl
|
||||
? [externalPayload.mediaUrl]
|
||||
: [];
|
||||
const replyToId = normalized.replyToId;
|
||||
const replyToId = externalPayload.replyToId;
|
||||
let hasSlackBlocks = false;
|
||||
if (
|
||||
channel === "slack" &&
|
||||
normalized.channelData?.slack &&
|
||||
typeof normalized.channelData.slack === "object" &&
|
||||
!Array.isArray(normalized.channelData.slack)
|
||||
externalPayload.channelData?.slack &&
|
||||
typeof externalPayload.channelData.slack === "object" &&
|
||||
!Array.isArray(externalPayload.channelData.slack)
|
||||
) {
|
||||
try {
|
||||
hasSlackBlocks = Boolean(
|
||||
parseSlackBlocksInput((normalized.channelData.slack as { blocks?: unknown }).blocks)
|
||||
parseSlackBlocksInput((externalPayload.channelData.slack as { blocks?: unknown }).blocks)
|
||||
?.length,
|
||||
);
|
||||
} catch {
|
||||
@@ -168,7 +175,7 @@ export async function routeReply(params: RouteReplyParams): Promise<RouteReplyRe
|
||||
channel: channelId,
|
||||
to,
|
||||
accountId: accountId ?? undefined,
|
||||
payloads: [normalized],
|
||||
payloads: [externalPayload],
|
||||
replyToId: resolvedReplyToId ?? null,
|
||||
threadId: resolvedThreadId,
|
||||
session: outboundSession,
|
||||
|
||||
@@ -76,6 +76,9 @@ export type ReplyPayload = {
|
||||
text?: string;
|
||||
mediaUrl?: string;
|
||||
mediaUrls?: string[];
|
||||
btw?: {
|
||||
question: string;
|
||||
};
|
||||
replyToId?: string;
|
||||
replyToTag?: boolean;
|
||||
/** True when [[reply_to_current]] was present but not yet mapped to a message id. */
|
||||
|
||||
Reference in New Issue
Block a user