Outbound: route sessions through channel plugins
This commit is contained in:
parent
826c592deb
commit
4079de21ce
@ -1,4 +1,4 @@
|
||||
import { requireBundledChannelPlugin } from "../channels/plugins/bundled.js";
|
||||
import { bundledChannelPlugins } from "../channels/plugins/bundled.js";
|
||||
import { setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import { createTestRegistry } from "../test-utils/channel-plugins.js";
|
||||
import { getChannelSetupWizardAdapter } from "./channel-setup/registry.js";
|
||||
@ -20,15 +20,11 @@ type PatchedSetupAdapterFields = {
|
||||
};
|
||||
|
||||
export function setDefaultChannelPluginRegistryForTests(): void {
|
||||
const channels = [
|
||||
{ pluginId: "discord", plugin: requireBundledChannelPlugin("discord"), source: "test" },
|
||||
{ pluginId: "feishu", plugin: requireBundledChannelPlugin("feishu"), source: "test" },
|
||||
{ pluginId: "slack", plugin: requireBundledChannelPlugin("slack"), source: "test" },
|
||||
{ pluginId: "telegram", plugin: requireBundledChannelPlugin("telegram"), source: "test" },
|
||||
{ pluginId: "whatsapp", plugin: requireBundledChannelPlugin("whatsapp"), source: "test" },
|
||||
{ pluginId: "signal", plugin: requireBundledChannelPlugin("signal"), source: "test" },
|
||||
{ pluginId: "imessage", plugin: requireBundledChannelPlugin("imessage"), source: "test" },
|
||||
] as unknown as Parameters<typeof createTestRegistry>[0];
|
||||
const channels = bundledChannelPlugins.map((plugin) => ({
|
||||
pluginId: plugin.id,
|
||||
plugin,
|
||||
source: "test" as const,
|
||||
})) as unknown as Parameters<typeof createTestRegistry>[0];
|
||||
setActivePluginRegistry(createTestRegistry(channels));
|
||||
}
|
||||
|
||||
|
||||
@ -44,6 +44,42 @@ describe("resolveOutboundSessionRoute", () => {
|
||||
chatType?: "direct" | "group";
|
||||
};
|
||||
}> = [
|
||||
{
|
||||
name: "WhatsApp group jid",
|
||||
cfg: baseConfig,
|
||||
channel: "whatsapp",
|
||||
target: "120363040000000000@g.us",
|
||||
expected: {
|
||||
sessionKey: "agent:main:whatsapp:group:120363040000000000@g.us",
|
||||
from: "120363040000000000@g.us",
|
||||
to: "120363040000000000@g.us",
|
||||
chatType: "group",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Matrix room target",
|
||||
cfg: baseConfig,
|
||||
channel: "matrix",
|
||||
target: "room:!ops:matrix.example",
|
||||
expected: {
|
||||
sessionKey: "agent:main:matrix:channel:!ops:matrix.example",
|
||||
from: "matrix:channel:!ops:matrix.example",
|
||||
to: "room:!ops:matrix.example",
|
||||
chatType: "channel",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "MSTeams conversation target",
|
||||
cfg: baseConfig,
|
||||
channel: "msteams",
|
||||
target: "conversation:19:meeting_abc@thread.tacv2",
|
||||
expected: {
|
||||
sessionKey: "agent:main:msteams:channel:19:meeting_abc@thread.tacv2",
|
||||
from: "msteams:channel:19:meeting_abc@thread.tacv2",
|
||||
to: "conversation:19:meeting_abc@thread.tacv2",
|
||||
chatType: "channel",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Slack thread",
|
||||
cfg: baseConfig,
|
||||
@ -115,6 +151,18 @@ describe("resolveOutboundSessionRoute", () => {
|
||||
sessionKey: "agent:main:direct:alice",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Nextcloud Talk room target",
|
||||
cfg: baseConfig,
|
||||
channel: "nextcloud-talk",
|
||||
target: "room:opsroom42",
|
||||
expected: {
|
||||
sessionKey: "agent:main:nextcloud-talk:group:opsroom42",
|
||||
from: "nextcloud-talk:room:opsroom42",
|
||||
to: "nextcloud-talk:opsroom42",
|
||||
chatType: "group",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "BlueBubbles chat_* prefix stripping",
|
||||
cfg: baseConfig,
|
||||
@ -125,6 +173,18 @@ describe("resolveOutboundSessionRoute", () => {
|
||||
from: "group:ABC123",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Zalo direct target",
|
||||
cfg: perChannelPeerCfg,
|
||||
channel: "zalo",
|
||||
target: "zl:123456",
|
||||
expected: {
|
||||
sessionKey: "agent:main:zalo:direct:123456",
|
||||
from: "zalo:123456",
|
||||
to: "zalo:123456",
|
||||
chatType: "direct",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Zalo Personal DM target",
|
||||
cfg: perChannelPeerCfg,
|
||||
@ -135,6 +195,30 @@ describe("resolveOutboundSessionRoute", () => {
|
||||
chatType: "direct",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Nostr prefixed target",
|
||||
cfg: perChannelPeerCfg,
|
||||
channel: "nostr",
|
||||
target: "nostr:npub1example",
|
||||
expected: {
|
||||
sessionKey: "agent:main:nostr:direct:npub1example",
|
||||
from: "nostr:npub1example",
|
||||
to: "nostr:npub1example",
|
||||
chatType: "direct",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Tlon group target",
|
||||
cfg: baseConfig,
|
||||
channel: "tlon",
|
||||
target: "group:~zod/main",
|
||||
expected: {
|
||||
sessionKey: "agent:main:tlon:group:chat/~zod/main",
|
||||
from: "tlon:group:chat/~zod/main",
|
||||
to: "tlon:chat/~zod/main",
|
||||
chatType: "group",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Slack mpim allowlist -> group key",
|
||||
cfg: slackMpimCfg,
|
||||
|
||||
@ -5,11 +5,8 @@ import type { ChannelId } from "../../channels/plugins/types.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { recordSessionMetaFromInbound, resolveStorePath } from "../../config/sessions.js";
|
||||
import type { RoutePeer } from "../../routing/resolve-route.js";
|
||||
import { resolveThreadSessionKeys } from "../../routing/session-key.js";
|
||||
import { isWhatsAppGroupJid, normalizeWhatsAppTarget } from "../../whatsapp/normalize.js";
|
||||
import { buildOutboundBaseSessionKey } from "./base-session-key.js";
|
||||
import type { ResolvedMessagingTarget } from "./target-resolver.js";
|
||||
import { normalizeOutboundThreadId } from "./thread-id.js";
|
||||
|
||||
export type OutboundSessionRoute = {
|
||||
sessionKey: string;
|
||||
@ -80,426 +77,6 @@ function buildBaseSessionKey(params: {
|
||||
return buildOutboundBaseSessionKey(params);
|
||||
}
|
||||
|
||||
function resolveWhatsAppSession(
|
||||
params: ResolveOutboundSessionRouteParams,
|
||||
): OutboundSessionRoute | null {
|
||||
const normalized = normalizeWhatsAppTarget(params.target);
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
const isGroup = isWhatsAppGroupJid(normalized);
|
||||
const peer: RoutePeer = {
|
||||
kind: isGroup ? "group" : "direct",
|
||||
id: normalized,
|
||||
};
|
||||
const baseSessionKey = buildBaseSessionKey({
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
channel: "whatsapp",
|
||||
accountId: params.accountId,
|
||||
peer,
|
||||
});
|
||||
return {
|
||||
sessionKey: baseSessionKey,
|
||||
baseSessionKey,
|
||||
peer,
|
||||
chatType: isGroup ? "group" : "direct",
|
||||
from: normalized,
|
||||
to: normalized,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveMatrixSession(
|
||||
params: ResolveOutboundSessionRouteParams,
|
||||
): OutboundSessionRoute | null {
|
||||
const stripped = stripProviderPrefix(params.target, "matrix");
|
||||
const isUser =
|
||||
params.resolvedTarget?.kind === "user" || stripped.startsWith("@") || /^user:/i.test(stripped);
|
||||
const rawId = stripKindPrefix(stripped);
|
||||
if (!rawId) {
|
||||
return null;
|
||||
}
|
||||
const peer: RoutePeer = { kind: isUser ? "direct" : "channel", id: rawId };
|
||||
const baseSessionKey = buildBaseSessionKey({
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
channel: "matrix",
|
||||
accountId: params.accountId,
|
||||
peer,
|
||||
});
|
||||
return {
|
||||
sessionKey: baseSessionKey,
|
||||
baseSessionKey,
|
||||
peer,
|
||||
chatType: isUser ? "direct" : "channel",
|
||||
from: isUser ? `matrix:${rawId}` : `matrix:channel:${rawId}`,
|
||||
to: `room:${rawId}`,
|
||||
};
|
||||
}
|
||||
|
||||
function buildSimpleBaseSession(params: {
|
||||
route: ResolveOutboundSessionRouteParams;
|
||||
channel: string;
|
||||
peer: RoutePeer;
|
||||
}) {
|
||||
const baseSessionKey = buildBaseSessionKey({
|
||||
cfg: params.route.cfg,
|
||||
agentId: params.route.agentId,
|
||||
channel: params.channel,
|
||||
accountId: params.route.accountId,
|
||||
peer: params.peer,
|
||||
});
|
||||
return { baseSessionKey, peer: params.peer };
|
||||
}
|
||||
|
||||
function resolveMSTeamsSession(
|
||||
params: ResolveOutboundSessionRouteParams,
|
||||
): OutboundSessionRoute | null {
|
||||
let trimmed = params.target.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
trimmed = trimmed.replace(/^(msteams|teams):/i, "").trim();
|
||||
|
||||
const lower = trimmed.toLowerCase();
|
||||
const isUser = lower.startsWith("user:");
|
||||
const rawId = stripKindPrefix(trimmed);
|
||||
if (!rawId) {
|
||||
return null;
|
||||
}
|
||||
const conversationId = rawId.split(";")[0] ?? rawId;
|
||||
const isChannel = !isUser && /@thread\.tacv2/i.test(conversationId);
|
||||
const peer: RoutePeer = {
|
||||
kind: isUser ? "direct" : isChannel ? "channel" : "group",
|
||||
id: conversationId,
|
||||
};
|
||||
const baseSessionKey = buildBaseSessionKey({
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
channel: "msteams",
|
||||
accountId: params.accountId,
|
||||
peer,
|
||||
});
|
||||
return {
|
||||
sessionKey: baseSessionKey,
|
||||
baseSessionKey,
|
||||
peer,
|
||||
chatType: isUser ? "direct" : isChannel ? "channel" : "group",
|
||||
from: isUser
|
||||
? `msteams:${conversationId}`
|
||||
: isChannel
|
||||
? `msteams:channel:${conversationId}`
|
||||
: `msteams:group:${conversationId}`,
|
||||
to: isUser ? `user:${conversationId}` : `conversation:${conversationId}`,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveMattermostSession(
|
||||
params: ResolveOutboundSessionRouteParams,
|
||||
): OutboundSessionRoute | null {
|
||||
let trimmed = params.target.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
trimmed = trimmed.replace(/^mattermost:/i, "").trim();
|
||||
const lower = trimmed.toLowerCase();
|
||||
const resolvedKind = params.resolvedTarget?.kind;
|
||||
const isUser =
|
||||
resolvedKind === "user" ||
|
||||
(resolvedKind !== "channel" &&
|
||||
resolvedKind !== "group" &&
|
||||
(lower.startsWith("user:") || trimmed.startsWith("@")));
|
||||
if (trimmed.startsWith("@")) {
|
||||
trimmed = trimmed.slice(1).trim();
|
||||
}
|
||||
const rawId = stripKindPrefix(trimmed);
|
||||
if (!rawId) {
|
||||
return null;
|
||||
}
|
||||
const { baseSessionKey, peer } = buildSimpleBaseSession({
|
||||
route: params,
|
||||
channel: "mattermost",
|
||||
peer: { kind: isUser ? "direct" : "channel", id: rawId },
|
||||
});
|
||||
const threadId = normalizeOutboundThreadId(params.replyToId ?? params.threadId);
|
||||
const threadKeys = resolveThreadSessionKeys({
|
||||
baseSessionKey,
|
||||
threadId,
|
||||
});
|
||||
return {
|
||||
sessionKey: threadKeys.sessionKey,
|
||||
baseSessionKey,
|
||||
peer,
|
||||
chatType: isUser ? "direct" : "channel",
|
||||
from: isUser ? `mattermost:${rawId}` : `mattermost:channel:${rawId}`,
|
||||
to: isUser ? `user:${rawId}` : `channel:${rawId}`,
|
||||
threadId,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveBlueBubblesSession(
|
||||
params: ResolveOutboundSessionRouteParams,
|
||||
): OutboundSessionRoute | null {
|
||||
const stripped = stripProviderPrefix(params.target, "bluebubbles");
|
||||
const lower = stripped.toLowerCase();
|
||||
const isGroup =
|
||||
lower.startsWith("chat_id:") ||
|
||||
lower.startsWith("chat_guid:") ||
|
||||
lower.startsWith("chat_identifier:") ||
|
||||
lower.startsWith("group:");
|
||||
const rawPeerId = isGroup
|
||||
? stripKindPrefix(stripped)
|
||||
: stripped.replace(/^(imessage|sms|auto):/i, "");
|
||||
// BlueBubbles inbound group ids omit chat_* prefixes; strip them to align sessions.
|
||||
const peerId = isGroup
|
||||
? rawPeerId.replace(/^(chat_id|chat_guid|chat_identifier):/i, "")
|
||||
: rawPeerId;
|
||||
if (!peerId) {
|
||||
return null;
|
||||
}
|
||||
const peer: RoutePeer = {
|
||||
kind: isGroup ? "group" : "direct",
|
||||
id: peerId,
|
||||
};
|
||||
const baseSessionKey = buildBaseSessionKey({
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
channel: "bluebubbles",
|
||||
accountId: params.accountId,
|
||||
peer,
|
||||
});
|
||||
return {
|
||||
sessionKey: baseSessionKey,
|
||||
baseSessionKey,
|
||||
peer,
|
||||
chatType: isGroup ? "group" : "direct",
|
||||
from: isGroup ? `group:${peerId}` : `bluebubbles:${peerId}`,
|
||||
to: `bluebubbles:${stripped}`,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveNextcloudTalkSession(
|
||||
params: ResolveOutboundSessionRouteParams,
|
||||
): OutboundSessionRoute | null {
|
||||
let trimmed = params.target.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
trimmed = trimmed.replace(/^(nextcloud-talk|nc-talk|nc):/i, "").trim();
|
||||
trimmed = trimmed.replace(/^room:/i, "").trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
const peer: RoutePeer = { kind: "group", id: trimmed };
|
||||
const baseSessionKey = buildBaseSessionKey({
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
channel: "nextcloud-talk",
|
||||
accountId: params.accountId,
|
||||
peer,
|
||||
});
|
||||
return {
|
||||
sessionKey: baseSessionKey,
|
||||
baseSessionKey,
|
||||
peer,
|
||||
chatType: "group",
|
||||
from: `nextcloud-talk:room:${trimmed}`,
|
||||
to: `nextcloud-talk:${trimmed}`,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveZaloSession(
|
||||
params: ResolveOutboundSessionRouteParams,
|
||||
): OutboundSessionRoute | null {
|
||||
return resolveZaloLikeSession(params, "zalo", /^(zl):/i);
|
||||
}
|
||||
|
||||
function resolveZaloLikeSession(
|
||||
params: ResolveOutboundSessionRouteParams,
|
||||
channel: "zalo" | "zalouser",
|
||||
aliasPrefix: RegExp,
|
||||
): OutboundSessionRoute | null {
|
||||
const trimmed = stripProviderPrefix(params.target, channel).replace(aliasPrefix, "").trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
const isGroup = trimmed.toLowerCase().startsWith("group:");
|
||||
const peerId = stripKindPrefix(trimmed);
|
||||
const peer: RoutePeer = { kind: isGroup ? "group" : "direct", id: peerId };
|
||||
const baseSessionKey = buildBaseSessionKey({
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
channel,
|
||||
accountId: params.accountId,
|
||||
peer,
|
||||
});
|
||||
return {
|
||||
sessionKey: baseSessionKey,
|
||||
baseSessionKey,
|
||||
peer,
|
||||
chatType: isGroup ? "group" : "direct",
|
||||
from: isGroup ? `${channel}:group:${peerId}` : `${channel}:${peerId}`,
|
||||
to: `${channel}:${peerId}`,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveZalouserSession(
|
||||
params: ResolveOutboundSessionRouteParams,
|
||||
): OutboundSessionRoute | null {
|
||||
// Keep DM vs group aligned with inbound sessions for Zalo Personal.
|
||||
return resolveZaloLikeSession(params, "zalouser", /^(zlu):/i);
|
||||
}
|
||||
|
||||
function resolveNostrSession(
|
||||
params: ResolveOutboundSessionRouteParams,
|
||||
): OutboundSessionRoute | null {
|
||||
const trimmed = stripProviderPrefix(params.target, "nostr").trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
const peer: RoutePeer = { kind: "direct", id: trimmed };
|
||||
const baseSessionKey = buildBaseSessionKey({
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
channel: "nostr",
|
||||
accountId: params.accountId,
|
||||
peer,
|
||||
});
|
||||
return {
|
||||
sessionKey: baseSessionKey,
|
||||
baseSessionKey,
|
||||
peer,
|
||||
chatType: "direct",
|
||||
from: `nostr:${trimmed}`,
|
||||
to: `nostr:${trimmed}`,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeTlonShip(raw: string): string {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) {
|
||||
return trimmed;
|
||||
}
|
||||
return trimmed.startsWith("~") ? trimmed : `~${trimmed}`;
|
||||
}
|
||||
|
||||
function resolveTlonSession(
|
||||
params: ResolveOutboundSessionRouteParams,
|
||||
): OutboundSessionRoute | null {
|
||||
let trimmed = stripProviderPrefix(params.target, "tlon");
|
||||
trimmed = trimmed.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
const lower = trimmed.toLowerCase();
|
||||
let isGroup =
|
||||
lower.startsWith("group:") || lower.startsWith("room:") || lower.startsWith("chat/");
|
||||
let peerId = trimmed;
|
||||
if (lower.startsWith("group:") || lower.startsWith("room:")) {
|
||||
peerId = trimmed.replace(/^(group|room):/i, "").trim();
|
||||
if (!peerId.startsWith("chat/")) {
|
||||
const parts = peerId.split("/").filter(Boolean);
|
||||
if (parts.length === 2) {
|
||||
peerId = `chat/${normalizeTlonShip(parts[0])}/${parts[1]}`;
|
||||
}
|
||||
}
|
||||
isGroup = true;
|
||||
} else if (lower.startsWith("dm:")) {
|
||||
peerId = normalizeTlonShip(trimmed.slice("dm:".length));
|
||||
isGroup = false;
|
||||
} else if (lower.startsWith("chat/")) {
|
||||
peerId = trimmed;
|
||||
isGroup = true;
|
||||
} else if (trimmed.includes("/")) {
|
||||
const parts = trimmed.split("/").filter(Boolean);
|
||||
if (parts.length === 2) {
|
||||
peerId = `chat/${normalizeTlonShip(parts[0])}/${parts[1]}`;
|
||||
isGroup = true;
|
||||
}
|
||||
} else {
|
||||
peerId = normalizeTlonShip(trimmed);
|
||||
}
|
||||
|
||||
const peer: RoutePeer = { kind: isGroup ? "group" : "direct", id: peerId };
|
||||
const baseSessionKey = buildBaseSessionKey({
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
channel: "tlon",
|
||||
accountId: params.accountId,
|
||||
peer,
|
||||
});
|
||||
return {
|
||||
sessionKey: baseSessionKey,
|
||||
baseSessionKey,
|
||||
peer,
|
||||
chatType: isGroup ? "group" : "direct",
|
||||
from: isGroup ? `tlon:group:${peerId}` : `tlon:${peerId}`,
|
||||
to: `tlon:${peerId}`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Feishu ID formats:
|
||||
* - oc_xxx: chat_id (can be group or DM, use chat_mode to distinguish or explicit dm:/group: prefix)
|
||||
* - ou_xxx: user open_id (DM)
|
||||
* - on_xxx: user union_id (DM)
|
||||
* - cli_xxx: app_id (not a valid send target)
|
||||
*/
|
||||
function resolveFeishuSession(
|
||||
params: ResolveOutboundSessionRouteParams,
|
||||
): OutboundSessionRoute | null {
|
||||
let trimmed = stripProviderPrefix(params.target, "feishu");
|
||||
trimmed = stripProviderPrefix(trimmed, "lark").trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lower = trimmed.toLowerCase();
|
||||
let isGroup = false;
|
||||
let typeExplicit = false;
|
||||
|
||||
if (lower.startsWith("group:") || lower.startsWith("chat:")) {
|
||||
trimmed = trimmed.replace(/^(group|chat):/i, "").trim();
|
||||
isGroup = true;
|
||||
typeExplicit = true;
|
||||
} else if (lower.startsWith("user:") || lower.startsWith("dm:")) {
|
||||
trimmed = trimmed.replace(/^(user|dm):/i, "").trim();
|
||||
isGroup = false;
|
||||
typeExplicit = true;
|
||||
}
|
||||
|
||||
const idLower = trimmed.toLowerCase();
|
||||
// Only infer type from ID prefix if not explicitly specified
|
||||
// Note: oc_ is a chat_id and can be either group or DM (must check chat_mode from API)
|
||||
// Only ou_/on_ can be reliably identified as user IDs (always DM)
|
||||
if (!typeExplicit) {
|
||||
if (idLower.startsWith("ou_") || idLower.startsWith("on_")) {
|
||||
isGroup = false;
|
||||
}
|
||||
// oc_ requires explicit prefix: dm:oc_xxx or group:oc_xxx
|
||||
}
|
||||
|
||||
const peer: RoutePeer = {
|
||||
kind: isGroup ? "group" : "direct",
|
||||
id: trimmed,
|
||||
};
|
||||
const baseSessionKey = buildBaseSessionKey({
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
channel: "feishu",
|
||||
accountId: params.accountId,
|
||||
peer,
|
||||
});
|
||||
return {
|
||||
sessionKey: baseSessionKey,
|
||||
baseSessionKey,
|
||||
peer,
|
||||
chatType: isGroup ? "group" : "direct",
|
||||
from: isGroup ? `feishu:group:${trimmed}` : `feishu:${trimmed}`,
|
||||
to: trimmed,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveFallbackSession(
|
||||
params: ResolveOutboundSessionRouteParams,
|
||||
): OutboundSessionRoute | null {
|
||||
@ -538,24 +115,6 @@ function resolveFallbackSession(
|
||||
};
|
||||
}
|
||||
|
||||
type OutboundSessionResolver = (
|
||||
params: ResolveOutboundSessionRouteParams,
|
||||
) => OutboundSessionRoute | null | Promise<OutboundSessionRoute | null>;
|
||||
|
||||
const OUTBOUND_SESSION_RESOLVERS: Partial<Record<ChannelId, OutboundSessionResolver>> = {
|
||||
whatsapp: resolveWhatsAppSession,
|
||||
matrix: resolveMatrixSession,
|
||||
msteams: resolveMSTeamsSession,
|
||||
mattermost: resolveMattermostSession,
|
||||
bluebubbles: resolveBlueBubblesSession,
|
||||
"nextcloud-talk": resolveNextcloudTalkSession,
|
||||
zalo: resolveZaloSession,
|
||||
zalouser: resolveZalouserSession,
|
||||
nostr: resolveNostrSession,
|
||||
tlon: resolveTlonSession,
|
||||
feishu: resolveFeishuSession,
|
||||
};
|
||||
|
||||
export async function resolveOutboundSessionRoute(
|
||||
params: ResolveOutboundSessionRouteParams,
|
||||
): Promise<OutboundSessionRoute | null> {
|
||||
@ -578,11 +137,7 @@ export async function resolveOutboundSessionRoute(
|
||||
if (pluginRoute) {
|
||||
return pluginRoute;
|
||||
}
|
||||
const resolver = OUTBOUND_SESSION_RESOLVERS[params.channel];
|
||||
if (!resolver) {
|
||||
return resolveFallbackSession(nextParams);
|
||||
}
|
||||
return await resolver(nextParams);
|
||||
return resolveFallbackSession(nextParams);
|
||||
}
|
||||
|
||||
export async function ensureOutboundSessionEntry(params: {
|
||||
|
||||
@ -975,6 +975,42 @@ describe("resolveOutboundSessionRoute", () => {
|
||||
chatType?: "direct" | "group";
|
||||
};
|
||||
}> = [
|
||||
{
|
||||
name: "WhatsApp group jid",
|
||||
cfg: baseConfig,
|
||||
channel: "whatsapp",
|
||||
target: "120363040000000000@g.us",
|
||||
expected: {
|
||||
sessionKey: "agent:main:whatsapp:group:120363040000000000@g.us",
|
||||
from: "120363040000000000@g.us",
|
||||
to: "120363040000000000@g.us",
|
||||
chatType: "group",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Matrix room target",
|
||||
cfg: baseConfig,
|
||||
channel: "matrix",
|
||||
target: "room:!ops:matrix.example",
|
||||
expected: {
|
||||
sessionKey: "agent:main:matrix:channel:!ops:matrix.example",
|
||||
from: "matrix:channel:!ops:matrix.example",
|
||||
to: "room:!ops:matrix.example",
|
||||
chatType: "channel",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "MSTeams conversation target",
|
||||
cfg: baseConfig,
|
||||
channel: "msteams",
|
||||
target: "conversation:19:meeting_abc@thread.tacv2",
|
||||
expected: {
|
||||
sessionKey: "agent:main:msteams:channel:19:meeting_abc@thread.tacv2",
|
||||
from: "msteams:channel:19:meeting_abc@thread.tacv2",
|
||||
to: "conversation:19:meeting_abc@thread.tacv2",
|
||||
chatType: "channel",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Slack thread",
|
||||
cfg: baseConfig,
|
||||
@ -1046,6 +1082,18 @@ describe("resolveOutboundSessionRoute", () => {
|
||||
sessionKey: "agent:main:direct:alice",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Nextcloud Talk room target",
|
||||
cfg: baseConfig,
|
||||
channel: "nextcloud-talk",
|
||||
target: "room:opsroom42",
|
||||
expected: {
|
||||
sessionKey: "agent:main:nextcloud-talk:group:opsroom42",
|
||||
from: "nextcloud-talk:room:opsroom42",
|
||||
to: "nextcloud-talk:opsroom42",
|
||||
chatType: "group",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "BlueBubbles chat_* prefix stripping",
|
||||
cfg: baseConfig,
|
||||
@ -1056,6 +1104,18 @@ describe("resolveOutboundSessionRoute", () => {
|
||||
from: "group:ABC123",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Zalo direct target",
|
||||
cfg: perChannelPeerCfg,
|
||||
channel: "zalo",
|
||||
target: "zl:123456",
|
||||
expected: {
|
||||
sessionKey: "agent:main:zalo:direct:123456",
|
||||
from: "zalo:123456",
|
||||
to: "zalo:123456",
|
||||
chatType: "direct",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Zalo Personal DM target",
|
||||
cfg: perChannelPeerCfg,
|
||||
@ -1066,6 +1126,30 @@ describe("resolveOutboundSessionRoute", () => {
|
||||
chatType: "direct",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Nostr prefixed target",
|
||||
cfg: perChannelPeerCfg,
|
||||
channel: "nostr",
|
||||
target: "nostr:npub1example",
|
||||
expected: {
|
||||
sessionKey: "agent:main:nostr:direct:npub1example",
|
||||
from: "nostr:npub1example",
|
||||
to: "nostr:npub1example",
|
||||
chatType: "direct",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Tlon group target",
|
||||
cfg: baseConfig,
|
||||
channel: "tlon",
|
||||
target: "group:~zod/main",
|
||||
expected: {
|
||||
sessionKey: "agent:main:tlon:group:chat/~zod/main",
|
||||
from: "tlon:group:chat/~zod/main",
|
||||
to: "tlon:chat/~zod/main",
|
||||
chatType: "group",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Slack mpim allowlist -> group key",
|
||||
cfg: slackMpimCfg,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user