fix: stop hardcoded channel fallback and auto-pick sole configured channel (#23357) (thanks @lbo728)

Co-authored-by: lbo728 <extreme0728@gmail.com>
This commit is contained in:
Peter Steinberger
2026-02-22 11:20:33 +01:00
parent e33d7fcd13
commit 1cd3b30907
18 changed files with 355 additions and 91 deletions

View File

@@ -1,5 +1,4 @@
import { describe, expect, it, vi } from "vitest";
import { DEFAULT_CHAT_CHANNEL } from "../../channels/registry.js";
import type { OpenClawConfig } from "../../config/config.js";
vi.mock("../../config/sessions.js", () => ({
@@ -223,16 +222,30 @@ describe("resolveDeliveryTarget", () => {
expect(result.threadId).toBe("thread-2");
});
it("falls back to default channel when selection probe fails", async () => {
it("uses single configured channel when neither explicit nor session channel exists", async () => {
setMainSessionEntry(undefined);
vi.mocked(resolveMessageChannelSelection).mockRejectedValueOnce(new Error("no selection"));
const result = await resolveForAgent({
cfg: makeCfg({ bindings: [] }),
target: { channel: "last", to: undefined },
});
expect(result.channel).toBe(DEFAULT_CHAT_CHANNEL);
expect(result.channel).toBe("telegram");
expect(result.error).toBeUndefined();
});
it("returns an error when channel selection is ambiguous", async () => {
setMainSessionEntry(undefined);
vi.mocked(resolveMessageChannelSelection).mockRejectedValueOnce(
new Error("Channel is required when multiple channels are configured: telegram, slack"),
);
const result = await resolveForAgent({
cfg: makeCfg({ bindings: [] }),
target: { channel: "last", to: undefined },
});
expect(result.channel).toBeUndefined();
expect(result.to).toBeUndefined();
expect(result.error?.message).toContain("Channel is required");
});
it("uses sessionKey thread entry before main session entry", async () => {
@@ -261,11 +274,12 @@ describe("resolveDeliveryTarget", () => {
expect(result.to).toBe("thread-chat");
});
it("uses channel selection result when no previous session target exists", async () => {
setMainSessionEntry(undefined);
vi.mocked(resolveMessageChannelSelection).mockResolvedValueOnce({
channel: "telegram",
configured: ["telegram"],
it("uses main session channel when channel=last and session route exists", async () => {
setMainSessionEntry({
sessionId: "sess-4",
updatedAt: 1000,
lastChannel: "telegram",
lastTo: "987654",
});
const result = await resolveForAgent({
@@ -274,7 +288,7 @@ describe("resolveDeliveryTarget", () => {
});
expect(result.channel).toBe("telegram");
expect(result.to).toBeUndefined();
expect(result.mode).toBe("implicit");
expect(result.to).toBe("987654");
expect(result.error).toBeUndefined();
});
});

View File

@@ -1,5 +1,4 @@
import type { ChannelId } from "../../channels/plugins/types.js";
import { DEFAULT_CHAT_CHANNEL } from "../../channels/registry.js";
import type { OpenClawConfig } from "../../config/config.js";
import {
loadSessionStore,
@@ -27,7 +26,7 @@ export async function resolveDeliveryTarget(
sessionKey?: string;
},
): Promise<{
channel: Exclude<OutboundChannel, "none">;
channel?: Exclude<OutboundChannel, "none">;
to?: string;
accountId?: string;
threadId?: string | number;
@@ -57,12 +56,20 @@ export async function resolveDeliveryTarget(
});
let fallbackChannel: Exclude<OutboundChannel, "none"> | undefined;
let channelResolutionError: Error | undefined;
if (!preliminary.channel) {
try {
const selection = await resolveMessageChannelSelection({ cfg });
fallbackChannel = selection.channel;
} catch {
fallbackChannel = preliminary.lastChannel ?? DEFAULT_CHAT_CHANNEL;
if (preliminary.lastChannel) {
fallbackChannel = preliminary.lastChannel;
} else {
try {
const selection = await resolveMessageChannelSelection({ cfg });
fallbackChannel = selection.channel;
} catch (err) {
const detail = err instanceof Error ? err.message : String(err);
channelResolutionError = new Error(
`${detail} Set delivery.channel explicitly or use a main session with a previous channel.`,
);
}
}
}
@@ -77,7 +84,7 @@ export async function resolveDeliveryTarget(
})
: preliminary;
const channel = resolved.channel ?? fallbackChannel ?? DEFAULT_CHAT_CHANNEL;
const channel = resolved.channel ?? fallbackChannel;
const mode = resolved.mode as "explicit" | "implicit";
let toCandidate = resolved.to;
@@ -105,6 +112,17 @@ export async function resolveDeliveryTarget(
? resolved.threadId
: undefined;
if (!channel) {
return {
channel: undefined,
to: undefined,
accountId,
threadId,
mode,
error: channelResolutionError,
};
}
if (!toCandidate) {
return {
channel,
@@ -112,6 +130,7 @@ export async function resolveDeliveryTarget(
accountId,
threadId,
mode,
error: channelResolutionError,
};
}
@@ -150,6 +169,6 @@ export async function resolveDeliveryTarget(
accountId,
threadId,
mode,
error: docked.ok ? undefined : docked.error,
error: docked.ok ? channelResolutionError : docked.error,
};
}

View File

@@ -75,9 +75,9 @@ import {
function matchesMessagingToolDeliveryTarget(
target: MessagingToolSend,
delivery: { channel: string; to?: string; accountId?: string },
delivery: { channel?: string; to?: string; accountId?: string },
): boolean {
if (!delivery.to || !target.to) {
if (!delivery.channel || !delivery.to || !target.to) {
return false;
}
const channel = delivery.channel.trim().toLowerCase();
@@ -611,6 +611,20 @@ export async function runCronIsolatedAgentTurn(params: {
logWarn(`[cron:${params.job.id}] ${resolvedDelivery.error.message}`);
return withRunSession({ status: "ok", summary, outputText, ...telemetry });
}
if (!resolvedDelivery.channel) {
const message = "cron delivery channel is missing";
if (!deliveryBestEffort) {
return withRunSession({
status: "error",
error: message,
summary,
outputText,
...telemetry,
});
}
logWarn(`[cron:${params.job.id}] ${message}`);
return withRunSession({ status: "ok", summary, outputText, ...telemetry });
}
if (!resolvedDelivery.to) {
const message = "cron delivery target is missing";
if (!deliveryBestEffort) {