Files
openclaw/src/utils/delivery-context.ts
2026-04-03 19:09:20 +01:00

236 lines
7.6 KiB
TypeScript

import { getChannelPlugin, normalizeChannelId } from "../channels/plugins/index.js";
import { normalizeAccountId } from "./account-id.js";
import { normalizeMessageChannel } from "./message-channel.js";
export type DeliveryContext = {
channel?: string;
to?: string;
accountId?: string;
threadId?: string | number;
};
export type DeliveryContextSessionSource = {
channel?: string;
lastChannel?: string;
lastTo?: string;
lastAccountId?: string;
lastThreadId?: string | number;
origin?: {
provider?: string;
accountId?: string;
threadId?: string | number;
};
deliveryContext?: DeliveryContext;
};
export function normalizeDeliveryContext(context?: DeliveryContext): DeliveryContext | undefined {
if (!context) {
return undefined;
}
const channel =
typeof context.channel === "string"
? (normalizeMessageChannel(context.channel) ?? context.channel.trim())
: undefined;
const to = typeof context.to === "string" ? context.to.trim() : undefined;
const accountId = normalizeAccountId(context.accountId);
const threadId =
typeof context.threadId === "number" && Number.isFinite(context.threadId)
? Math.trunc(context.threadId)
: typeof context.threadId === "string"
? context.threadId.trim()
: undefined;
const normalizedThreadId =
typeof threadId === "string" ? (threadId ? threadId : undefined) : threadId;
if (!channel && !to && !accountId && normalizedThreadId == null) {
return undefined;
}
const normalized: DeliveryContext = {
channel: channel || undefined,
to: to || undefined,
accountId,
};
if (normalizedThreadId != null) {
normalized.threadId = normalizedThreadId;
}
return normalized;
}
export function formatConversationTarget(params: {
channel?: string;
conversationId?: string | number;
parentConversationId?: string | number;
}): string | undefined {
const channel =
typeof params.channel === "string"
? (normalizeMessageChannel(params.channel) ?? params.channel.trim())
: undefined;
const conversationId =
typeof params.conversationId === "number" && Number.isFinite(params.conversationId)
? String(Math.trunc(params.conversationId))
: typeof params.conversationId === "string"
? params.conversationId.trim()
: undefined;
if (!channel || !conversationId) {
return undefined;
}
const parentConversationId =
typeof params.parentConversationId === "number" && Number.isFinite(params.parentConversationId)
? String(Math.trunc(params.parentConversationId))
: typeof params.parentConversationId === "string"
? params.parentConversationId.trim()
: undefined;
const pluginTarget = normalizeChannelId(channel)
? getChannelPlugin(normalizeChannelId(channel)!)?.messaging?.resolveDeliveryTarget?.({
conversationId,
parentConversationId,
})
: null;
if (pluginTarget?.to?.trim()) {
return pluginTarget.to.trim();
}
return `channel:${conversationId}`;
}
export function resolveConversationDeliveryTarget(params: {
channel?: string;
conversationId?: string | number;
parentConversationId?: string | number;
}): { to?: string; threadId?: string } {
const channel =
typeof params.channel === "string"
? (normalizeMessageChannel(params.channel) ?? params.channel.trim())
: undefined;
const conversationId =
typeof params.conversationId === "number" && Number.isFinite(params.conversationId)
? String(Math.trunc(params.conversationId))
: typeof params.conversationId === "string"
? params.conversationId.trim()
: undefined;
const parentConversationId =
typeof params.parentConversationId === "number" && Number.isFinite(params.parentConversationId)
? String(Math.trunc(params.parentConversationId))
: typeof params.parentConversationId === "string"
? params.parentConversationId.trim()
: undefined;
const pluginTarget =
channel && conversationId
? getChannelPlugin(
normalizeChannelId(channel) ?? channel,
)?.messaging?.resolveDeliveryTarget?.({
conversationId,
parentConversationId,
})
: null;
if (pluginTarget) {
return {
...(pluginTarget.to?.trim() ? { to: pluginTarget.to.trim() } : {}),
...(pluginTarget.threadId?.trim() ? { threadId: pluginTarget.threadId.trim() } : {}),
};
}
const to = formatConversationTarget(params);
return { to };
}
export function normalizeSessionDeliveryFields(source?: DeliveryContextSessionSource): {
deliveryContext?: DeliveryContext;
lastChannel?: string;
lastTo?: string;
lastAccountId?: string;
lastThreadId?: string | number;
} {
if (!source) {
return {
deliveryContext: undefined,
lastChannel: undefined,
lastTo: undefined,
lastAccountId: undefined,
lastThreadId: undefined,
};
}
const merged = mergeDeliveryContext(
normalizeDeliveryContext({
channel: source.lastChannel ?? source.channel,
to: source.lastTo,
accountId: source.lastAccountId,
threadId: source.lastThreadId,
}),
normalizeDeliveryContext(source.deliveryContext),
);
if (!merged) {
return {
deliveryContext: undefined,
lastChannel: undefined,
lastTo: undefined,
lastAccountId: undefined,
lastThreadId: undefined,
};
}
return {
deliveryContext: merged,
lastChannel: merged.channel,
lastTo: merged.to,
lastAccountId: merged.accountId,
lastThreadId: merged.threadId,
};
}
export function deliveryContextFromSession(
entry?: DeliveryContextSessionSource,
): DeliveryContext | undefined {
if (!entry) {
return undefined;
}
const source: DeliveryContextSessionSource = {
channel: entry.channel ?? entry.origin?.provider,
lastChannel: entry.lastChannel,
lastTo: entry.lastTo,
lastAccountId: entry.lastAccountId ?? entry.origin?.accountId,
lastThreadId: entry.lastThreadId ?? entry.deliveryContext?.threadId ?? entry.origin?.threadId,
origin: entry.origin,
deliveryContext: entry.deliveryContext,
};
return normalizeSessionDeliveryFields(source).deliveryContext;
}
export function mergeDeliveryContext(
primary?: DeliveryContext,
fallback?: DeliveryContext,
): DeliveryContext | undefined {
const normalizedPrimary = normalizeDeliveryContext(primary);
const normalizedFallback = normalizeDeliveryContext(fallback);
if (!normalizedPrimary && !normalizedFallback) {
return undefined;
}
const channelsConflict =
normalizedPrimary?.channel &&
normalizedFallback?.channel &&
normalizedPrimary.channel !== normalizedFallback.channel;
return normalizeDeliveryContext({
channel: normalizedPrimary?.channel ?? normalizedFallback?.channel,
// Keep route fields paired to their channel; avoid crossing fields between
// unrelated channels during session context merges.
to: channelsConflict
? normalizedPrimary?.to
: (normalizedPrimary?.to ?? normalizedFallback?.to),
accountId: channelsConflict
? normalizedPrimary?.accountId
: (normalizedPrimary?.accountId ?? normalizedFallback?.accountId),
threadId: channelsConflict
? normalizedPrimary?.threadId
: (normalizedPrimary?.threadId ?? normalizedFallback?.threadId),
});
}
export function deliveryContextKey(context?: DeliveryContext): string | undefined {
const normalized = normalizeDeliveryContext(context);
if (!normalized?.channel || !normalized?.to) {
return undefined;
}
const threadId =
normalized.threadId != null && normalized.threadId !== "" ? String(normalized.threadId) : "";
return `${normalized.channel}|${normalized.to}|${normalized.accountId ?? ""}|${threadId}`;
}