mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 12:41:37 +00:00
[codex] Fix main-session web UI reply routing to Telegram (openclaw#29328) thanks @BeeSting50
Verified: - pnpm test src/auto-reply/reply/dispatch-from-config.test.ts src/gateway/server-methods/chat.directive-tags.test.ts - pnpm exec oxfmt --check src/auto-reply/reply/dispatch-from-config.test.ts src/gateway/server-methods/chat.directive-tags.test.ts src/auto-reply/reply/dispatch-from-config.ts src/gateway/server-methods/chat.ts CHANGELOG.md - CI note: non-required check "check" failed on unrelated src/slack/monitor/events/messages.ts TS errors outside this PR scope. Co-authored-by: BeeSting50 <85285887+BeeSting50@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
@@ -77,7 +77,9 @@ vi.mock("./route-reply.js", () => ({
|
||||
isRoutableChannel: (channel: string | undefined) =>
|
||||
Boolean(
|
||||
channel &&
|
||||
["telegram", "slack", "discord", "signal", "imessage", "whatsapp"].includes(channel),
|
||||
["telegram", "slack", "discord", "signal", "imessage", "whatsapp", "feishu"].includes(
|
||||
channel,
|
||||
),
|
||||
),
|
||||
routeReply: mocks.routeReply,
|
||||
}));
|
||||
@@ -327,6 +329,73 @@ describe("dispatchReplyFromConfig", () => {
|
||||
await dispatchReplyFromConfig({ ctx, cfg, dispatcher, replyResolver });
|
||||
});
|
||||
|
||||
it("routes when provider is webchat but surface carries originating channel metadata", async () => {
|
||||
setNoAbort();
|
||||
mocks.routeReply.mockClear();
|
||||
const cfg = emptyConfig;
|
||||
const dispatcher = createDispatcher();
|
||||
const ctx = buildTestCtx({
|
||||
Provider: "webchat",
|
||||
Surface: "telegram",
|
||||
OriginatingChannel: "telegram",
|
||||
OriginatingTo: "telegram:999",
|
||||
});
|
||||
|
||||
const replyResolver = async () => ({ text: "hi" }) satisfies ReplyPayload;
|
||||
await dispatchReplyFromConfig({ ctx, cfg, dispatcher, replyResolver });
|
||||
|
||||
expect(dispatcher.sendFinalReply).not.toHaveBeenCalled();
|
||||
expect(mocks.routeReply).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
channel: "telegram",
|
||||
to: "telegram:999",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("routes Feishu replies when provider is webchat and origin metadata points to Feishu", async () => {
|
||||
setNoAbort();
|
||||
mocks.routeReply.mockClear();
|
||||
const cfg = emptyConfig;
|
||||
const dispatcher = createDispatcher();
|
||||
const ctx = buildTestCtx({
|
||||
Provider: "webchat",
|
||||
Surface: "feishu",
|
||||
OriginatingChannel: "feishu",
|
||||
OriginatingTo: "ou_feishu_direct_123",
|
||||
});
|
||||
|
||||
const replyResolver = async () => ({ text: "hi" }) satisfies ReplyPayload;
|
||||
await dispatchReplyFromConfig({ ctx, cfg, dispatcher, replyResolver });
|
||||
|
||||
expect(dispatcher.sendFinalReply).not.toHaveBeenCalled();
|
||||
expect(mocks.routeReply).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
channel: "feishu",
|
||||
to: "ou_feishu_direct_123",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not route when provider already matches originating channel", async () => {
|
||||
setNoAbort();
|
||||
mocks.routeReply.mockClear();
|
||||
const cfg = emptyConfig;
|
||||
const dispatcher = createDispatcher();
|
||||
const ctx = buildTestCtx({
|
||||
Provider: "telegram",
|
||||
Surface: "webchat",
|
||||
OriginatingChannel: "telegram",
|
||||
OriginatingTo: "telegram:999",
|
||||
});
|
||||
|
||||
const replyResolver = async () => ({ text: "hi" }) satisfies ReplyPayload;
|
||||
await dispatchReplyFromConfig({ ctx, cfg, dispatcher, replyResolver });
|
||||
|
||||
expect(mocks.routeReply).not.toHaveBeenCalled();
|
||||
expect(dispatcher.sendFinalReply).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("routes media-only tool results when summaries are suppressed", async () => {
|
||||
setNoAbort();
|
||||
mocks.routeReply.mockClear();
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js";
|
||||
import { resolveSendPolicy } from "../../sessions/send-policy.js";
|
||||
import { maybeApplyTtsToPayload, normalizeTtsAutoMode, resolveTtsConfig } from "../../tts/tts.js";
|
||||
import { INTERNAL_MESSAGE_CHANNEL } from "../../utils/message-channel.js";
|
||||
import { INTERNAL_MESSAGE_CHANNEL, normalizeMessageChannel } from "../../utils/message-channel.js";
|
||||
import { getReplyFromConfig } from "../reply.js";
|
||||
import type { FinalizedMsgContext } from "../templating.js";
|
||||
import type { GetReplyOptions, ReplyPayload } from "../types.js";
|
||||
@@ -249,9 +249,12 @@ export async function dispatchReplyFromConfig(params: {
|
||||
// flow when the provider handles its own messages.
|
||||
//
|
||||
// Debug: `pnpm test src/auto-reply/reply/dispatch-from-config.test.ts`
|
||||
const originatingChannel = ctx.OriginatingChannel;
|
||||
const originatingChannel = normalizeMessageChannel(ctx.OriginatingChannel);
|
||||
const originatingTo = ctx.OriginatingTo;
|
||||
const currentSurface = (ctx.Surface ?? ctx.Provider)?.toLowerCase();
|
||||
const providerChannel = normalizeMessageChannel(ctx.Provider);
|
||||
const surfaceChannel = normalizeMessageChannel(ctx.Surface);
|
||||
// Prefer provider channel because surface may carry origin metadata in relayed flows.
|
||||
const currentSurface = providerChannel ?? surfaceChannel;
|
||||
const shouldRouteToOriginating = Boolean(
|
||||
isRoutableChannel(originatingChannel) && originatingTo && originatingChannel !== currentSurface,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user