Files
openclaw/src/channels/conversation-binding-context.ts

217 lines
6.5 KiB
TypeScript

import { normalizeConversationText } from "../acp/conversation-id.js";
import type { OpenClawConfig } from "../config/config.js";
import { resolveConversationIdFromTargets } from "../infra/outbound/conversation-id.js";
import { getActivePluginChannelRegistry } from "../plugins/runtime.js";
import { parseExplicitTargetForChannel } from "./plugins/target-parsing.js";
import type { ChannelPlugin } from "./plugins/types.js";
import { normalizeAnyChannelId, normalizeChannelId } from "./registry.js";
export type ConversationBindingContext = {
channel: string;
accountId: string;
conversationId: string;
parentConversationId?: string;
threadId?: string;
};
export type ResolveConversationBindingContextInput = {
cfg: OpenClawConfig;
channel?: string | null;
accountId?: string | null;
chatType?: string | null;
threadId?: string | number | null;
threadParentId?: string | null;
senderId?: string | null;
sessionKey?: string | null;
parentSessionKey?: string | null;
originatingTo?: string | null;
commandTo?: string | null;
fallbackTo?: string | null;
from?: string | null;
nativeChannelId?: string | null;
};
const CANONICAL_TARGET_PREFIXES = [
"user:",
"channel:",
"conversation:",
"group:",
"room:",
"dm:",
"spaces/",
] as const;
function normalizeText(value: unknown): string | undefined {
const normalized = normalizeConversationText(value);
return normalized || undefined;
}
function getLoadedChannelPlugin(rawChannel: string): ChannelPlugin | undefined {
const normalized = normalizeAnyChannelId(rawChannel) ?? normalizeText(rawChannel);
if (!normalized) {
return undefined;
}
return getActivePluginChannelRegistry()?.channels.find((entry) => entry.plugin.id === normalized)
?.plugin;
}
function resolveChannelTargetId(params: {
channel: string;
target?: string | null;
}): string | undefined {
const target = normalizeText(params.target);
if (!target) {
return undefined;
}
const lower = target.toLowerCase();
const channelPrefix = `${params.channel}:`;
if (lower.startsWith(channelPrefix)) {
return resolveChannelTargetId({
channel: params.channel,
target: target.slice(channelPrefix.length),
});
}
if (CANONICAL_TARGET_PREFIXES.some((prefix) => lower.startsWith(prefix))) {
return target;
}
const parsed = parseExplicitTargetForChannel(params.channel, target);
const parsedTarget = normalizeText(parsed?.to);
if (parsedTarget) {
return (
resolveConversationIdFromTargets({
targets: [parsedTarget],
}) ?? parsedTarget
);
}
const explicitConversationId = resolveConversationIdFromTargets({
targets: [target],
});
return explicitConversationId ?? target;
}
function buildThreadingContext(params: {
fallbackTo?: string;
originatingTo?: string;
threadId?: string;
from?: string;
chatType?: string;
nativeChannelId?: string;
}) {
const to = normalizeText(params.originatingTo) ?? normalizeText(params.fallbackTo);
return {
...(to ? { To: to } : {}),
...(params.from ? { From: params.from } : {}),
...(params.chatType ? { ChatType: params.chatType } : {}),
...(params.threadId ? { MessageThreadId: params.threadId } : {}),
...(params.nativeChannelId ? { NativeChannelId: params.nativeChannelId } : {}),
};
}
export function resolveConversationBindingContext(
params: ResolveConversationBindingContextInput,
): ConversationBindingContext | null {
const channel =
normalizeAnyChannelId(params.channel) ??
normalizeChannelId(params.channel) ??
normalizeText(params.channel)?.toLowerCase();
if (!channel) {
return null;
}
const accountId = normalizeText(params.accountId) || "default";
const threadId = normalizeText(params.threadId != null ? String(params.threadId) : undefined);
const loadedPlugin = getLoadedChannelPlugin(channel);
const resolvedByProvider = loadedPlugin?.bindings?.resolveCommandConversation?.({
accountId,
threadId,
threadParentId: normalizeText(params.threadParentId),
senderId: normalizeText(params.senderId),
sessionKey: normalizeText(params.sessionKey),
parentSessionKey: normalizeText(params.parentSessionKey),
originatingTo: params.originatingTo ?? undefined,
commandTo: params.commandTo ?? undefined,
fallbackTo: params.fallbackTo ?? undefined,
});
if (resolvedByProvider?.conversationId) {
const resolvedParentConversationId =
channel === "telegram" && !threadId && !resolvedByProvider.parentConversationId
? resolvedByProvider.conversationId
: resolvedByProvider.parentConversationId;
return {
channel,
accountId,
conversationId: resolvedByProvider.conversationId,
...(resolvedParentConversationId
? { parentConversationId: resolvedParentConversationId }
: {}),
...(threadId ? { threadId } : {}),
};
}
const focusedBinding = loadedPlugin?.threading?.resolveFocusedBinding?.({
cfg: params.cfg,
accountId,
context: buildThreadingContext({
fallbackTo: params.fallbackTo ?? undefined,
originatingTo: params.originatingTo ?? undefined,
threadId,
from: normalizeText(params.from),
chatType: normalizeText(params.chatType),
nativeChannelId: normalizeText(params.nativeChannelId),
}),
});
if (focusedBinding?.conversationId) {
return {
channel,
accountId,
conversationId: focusedBinding.conversationId,
...(focusedBinding.parentConversationId
? { parentConversationId: focusedBinding.parentConversationId }
: {}),
...(threadId ? { threadId } : {}),
};
}
const baseConversationId =
resolveChannelTargetId({
channel,
target: params.originatingTo,
}) ??
resolveChannelTargetId({
channel,
target: params.commandTo,
}) ??
resolveChannelTargetId({
channel,
target: params.fallbackTo,
});
const parentConversationId =
resolveChannelTargetId({
channel,
target: params.threadParentId,
}) ??
(threadId && baseConversationId && baseConversationId !== threadId
? baseConversationId
: undefined);
const conversationId = threadId || baseConversationId;
if (!conversationId) {
return null;
}
const normalizedParentConversationId =
channel === "telegram" && !threadId && !parentConversationId
? conversationId
: parentConversationId;
return {
channel,
accountId,
conversationId,
...(normalizedParentConversationId
? { parentConversationId: normalizedParentConversationId }
: {}),
...(threadId ? { threadId } : {}),
};
}