242 lines
7.0 KiB
TypeScript
242 lines
7.0 KiB
TypeScript
import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js";
|
|
import { formatCliCommand } from "../../cli/command-format.js";
|
|
import type { ChannelId, ChannelOutboundTargetMode } from "../../channels/plugins/types.js";
|
|
import type { ClawdbotConfig } from "../../config/config.js";
|
|
import type { SessionEntry } from "../../config/sessions.js";
|
|
import type { AgentDefaultsConfig } from "../../config/types.agent-defaults.js";
|
|
import { deliveryContextFromSession } from "../../utils/delivery-context.js";
|
|
import type {
|
|
DeliverableMessageChannel,
|
|
GatewayMessageChannel,
|
|
} from "../../utils/message-channel.js";
|
|
import {
|
|
INTERNAL_MESSAGE_CHANNEL,
|
|
isDeliverableMessageChannel,
|
|
normalizeMessageChannel,
|
|
} from "../../utils/message-channel.js";
|
|
import { missingTargetError } from "./target-errors.js";
|
|
|
|
export type OutboundChannel = DeliverableMessageChannel | "none";
|
|
|
|
export type HeartbeatTarget = OutboundChannel | "last";
|
|
|
|
export type OutboundTarget = {
|
|
channel: OutboundChannel;
|
|
to?: string;
|
|
reason?: string;
|
|
accountId?: string;
|
|
lastChannel?: DeliverableMessageChannel;
|
|
lastAccountId?: string;
|
|
};
|
|
|
|
export type OutboundTargetResolution = { ok: true; to: string } | { ok: false; error: Error };
|
|
|
|
export type SessionDeliveryTarget = {
|
|
channel?: DeliverableMessageChannel;
|
|
to?: string;
|
|
accountId?: string;
|
|
mode: ChannelOutboundTargetMode;
|
|
lastChannel?: DeliverableMessageChannel;
|
|
lastTo?: string;
|
|
lastAccountId?: string;
|
|
};
|
|
|
|
export function resolveSessionDeliveryTarget(params: {
|
|
entry?: SessionEntry;
|
|
requestedChannel?: GatewayMessageChannel | "last";
|
|
explicitTo?: string;
|
|
fallbackChannel?: DeliverableMessageChannel;
|
|
allowMismatchedLastTo?: boolean;
|
|
mode?: ChannelOutboundTargetMode;
|
|
}): SessionDeliveryTarget {
|
|
const context = deliveryContextFromSession(params.entry);
|
|
const lastChannel =
|
|
context?.channel && isDeliverableMessageChannel(context.channel) ? context.channel : undefined;
|
|
const lastTo = context?.to;
|
|
const lastAccountId = context?.accountId;
|
|
|
|
const rawRequested = params.requestedChannel ?? "last";
|
|
const requested = rawRequested === "last" ? "last" : normalizeMessageChannel(rawRequested);
|
|
const requestedChannel =
|
|
requested === "last"
|
|
? "last"
|
|
: requested && isDeliverableMessageChannel(requested)
|
|
? requested
|
|
: undefined;
|
|
|
|
const explicitTo =
|
|
typeof params.explicitTo === "string" && params.explicitTo.trim()
|
|
? params.explicitTo.trim()
|
|
: undefined;
|
|
|
|
let channel = requestedChannel === "last" ? lastChannel : requestedChannel;
|
|
if (!channel && params.fallbackChannel && isDeliverableMessageChannel(params.fallbackChannel)) {
|
|
channel = params.fallbackChannel;
|
|
}
|
|
|
|
let to = explicitTo;
|
|
if (!to && lastTo) {
|
|
if (channel && channel === lastChannel) {
|
|
to = lastTo;
|
|
} else if (params.allowMismatchedLastTo) {
|
|
to = lastTo;
|
|
}
|
|
}
|
|
|
|
const accountId = channel && channel === lastChannel ? lastAccountId : undefined;
|
|
const mode = params.mode ?? (explicitTo ? "explicit" : "implicit");
|
|
|
|
return {
|
|
channel,
|
|
to,
|
|
accountId,
|
|
mode,
|
|
lastChannel,
|
|
lastTo,
|
|
lastAccountId,
|
|
};
|
|
}
|
|
|
|
// Channel docking: prefer plugin.outbound.resolveTarget + allowFrom to normalize destinations.
|
|
export function resolveOutboundTarget(params: {
|
|
channel: GatewayMessageChannel;
|
|
to?: string;
|
|
allowFrom?: string[];
|
|
cfg?: ClawdbotConfig;
|
|
accountId?: string | null;
|
|
mode?: ChannelOutboundTargetMode;
|
|
}): OutboundTargetResolution {
|
|
if (params.channel === INTERNAL_MESSAGE_CHANNEL) {
|
|
return {
|
|
ok: false,
|
|
error: new Error(
|
|
`Delivering to WebChat is not supported via \`${formatCliCommand("clawdbot agent")}\`; use WhatsApp/Telegram or run with --deliver=false.`,
|
|
),
|
|
};
|
|
}
|
|
|
|
const plugin = getChannelPlugin(params.channel as ChannelId);
|
|
if (!plugin) {
|
|
return {
|
|
ok: false,
|
|
error: new Error(`Unsupported channel: ${params.channel}`),
|
|
};
|
|
}
|
|
|
|
const allowFrom =
|
|
params.allowFrom ??
|
|
(params.cfg && plugin.config.resolveAllowFrom
|
|
? plugin.config.resolveAllowFrom({
|
|
cfg: params.cfg,
|
|
accountId: params.accountId ?? undefined,
|
|
})
|
|
: undefined);
|
|
|
|
const resolveTarget = plugin.outbound?.resolveTarget;
|
|
if (resolveTarget) {
|
|
return resolveTarget({
|
|
cfg: params.cfg,
|
|
to: params.to,
|
|
allowFrom,
|
|
accountId: params.accountId ?? undefined,
|
|
mode: params.mode ?? "explicit",
|
|
});
|
|
}
|
|
|
|
const trimmed = params.to?.trim();
|
|
if (trimmed) {
|
|
return { ok: true, to: trimmed };
|
|
}
|
|
const hint = plugin.messaging?.targetResolver?.hint;
|
|
return {
|
|
ok: false,
|
|
error: missingTargetError(plugin.meta.label ?? params.channel, hint),
|
|
};
|
|
}
|
|
|
|
export function resolveHeartbeatDeliveryTarget(params: {
|
|
cfg: ClawdbotConfig;
|
|
entry?: SessionEntry;
|
|
heartbeat?: AgentDefaultsConfig["heartbeat"];
|
|
}): OutboundTarget {
|
|
const { cfg, entry } = params;
|
|
const heartbeat = params.heartbeat ?? cfg.agents?.defaults?.heartbeat;
|
|
const rawTarget = heartbeat?.target;
|
|
let target: HeartbeatTarget = "last";
|
|
if (rawTarget === "none" || rawTarget === "last") {
|
|
target = rawTarget;
|
|
} else if (typeof rawTarget === "string") {
|
|
const normalized = normalizeChannelId(rawTarget);
|
|
if (normalized) target = normalized;
|
|
}
|
|
|
|
if (target === "none") {
|
|
const base = resolveSessionDeliveryTarget({ entry });
|
|
return {
|
|
channel: "none",
|
|
reason: "target-none",
|
|
accountId: undefined,
|
|
lastChannel: base.lastChannel,
|
|
lastAccountId: base.lastAccountId,
|
|
};
|
|
}
|
|
|
|
const resolvedTarget = resolveSessionDeliveryTarget({
|
|
entry,
|
|
requestedChannel: target === "last" ? "last" : target,
|
|
explicitTo: heartbeat?.to,
|
|
mode: "heartbeat",
|
|
});
|
|
|
|
if (!resolvedTarget.channel || !resolvedTarget.to) {
|
|
return {
|
|
channel: "none",
|
|
reason: "no-target",
|
|
accountId: resolvedTarget.accountId,
|
|
lastChannel: resolvedTarget.lastChannel,
|
|
lastAccountId: resolvedTarget.lastAccountId,
|
|
};
|
|
}
|
|
|
|
const resolved = resolveOutboundTarget({
|
|
channel: resolvedTarget.channel,
|
|
to: resolvedTarget.to,
|
|
cfg,
|
|
accountId: resolvedTarget.accountId,
|
|
mode: "heartbeat",
|
|
});
|
|
if (!resolved.ok) {
|
|
return {
|
|
channel: "none",
|
|
reason: "no-target",
|
|
accountId: resolvedTarget.accountId,
|
|
lastChannel: resolvedTarget.lastChannel,
|
|
lastAccountId: resolvedTarget.lastAccountId,
|
|
};
|
|
}
|
|
|
|
let reason: string | undefined;
|
|
const plugin = getChannelPlugin(resolvedTarget.channel as ChannelId);
|
|
if (plugin?.config.resolveAllowFrom) {
|
|
const explicit = resolveOutboundTarget({
|
|
channel: resolvedTarget.channel,
|
|
to: resolvedTarget.to,
|
|
cfg,
|
|
accountId: resolvedTarget.accountId,
|
|
mode: "explicit",
|
|
});
|
|
if (explicit.ok && explicit.to !== resolved.to) {
|
|
reason = "allowFrom-fallback";
|
|
}
|
|
}
|
|
|
|
return {
|
|
channel: resolvedTarget.channel,
|
|
to: resolved.to,
|
|
reason,
|
|
accountId: resolvedTarget.accountId,
|
|
lastChannel: resolvedTarget.lastChannel,
|
|
lastAccountId: resolvedTarget.lastAccountId,
|
|
};
|
|
}
|