feat: add session.identityLinks for cross-platform DM session linking (#1033)
Co-authored-by: Shadow <shadow@clawd.bot>
This commit is contained in:
@@ -44,6 +44,42 @@ describe("resolveAgentRoute", () => {
|
||||
expect(route.sessionKey).toBe("agent:main:whatsapp:dm:+15551234567");
|
||||
});
|
||||
|
||||
test("identityLinks collapses per-peer DM sessions across providers", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
session: {
|
||||
dmScope: "per-peer",
|
||||
identityLinks: {
|
||||
alice: ["telegram:111111111", "discord:222222222222222222"],
|
||||
},
|
||||
},
|
||||
};
|
||||
const route = resolveAgentRoute({
|
||||
cfg,
|
||||
channel: "telegram",
|
||||
accountId: null,
|
||||
peer: { kind: "dm", id: "111111111" },
|
||||
});
|
||||
expect(route.sessionKey).toBe("agent:main:dm:alice");
|
||||
});
|
||||
|
||||
test("identityLinks applies to per-channel-peer DM sessions", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
session: {
|
||||
dmScope: "per-channel-peer",
|
||||
identityLinks: {
|
||||
alice: ["telegram:111111111", "discord:222222222222222222"],
|
||||
},
|
||||
},
|
||||
};
|
||||
const route = resolveAgentRoute({
|
||||
cfg,
|
||||
channel: "discord",
|
||||
accountId: null,
|
||||
peer: { kind: "dm", id: "222222222222222222" },
|
||||
});
|
||||
expect(route.sessionKey).toBe("agent:main:discord:dm:alice");
|
||||
});
|
||||
|
||||
test("peer binding wins over account binding", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
bindings: [
|
||||
|
||||
@@ -70,6 +70,7 @@ export function buildAgentSessionKey(params: {
|
||||
peer?: RoutePeer | null;
|
||||
/** DM session scope. */
|
||||
dmScope?: "main" | "per-peer" | "per-channel-peer";
|
||||
identityLinks?: Record<string, string[]>;
|
||||
}): string {
|
||||
const channel = normalizeToken(params.channel) || "unknown";
|
||||
const peer = params.peer;
|
||||
@@ -80,6 +81,7 @@ export function buildAgentSessionKey(params: {
|
||||
peerKind: peer?.kind ?? "dm",
|
||||
peerId: peer ? normalizeId(peer.id) || "unknown" : null,
|
||||
dmScope: params.dmScope,
|
||||
identityLinks: params.identityLinks,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -153,6 +155,7 @@ export function resolveAgentRoute(input: ResolveAgentRouteInput): ResolvedAgentR
|
||||
});
|
||||
|
||||
const dmScope = input.cfg.session?.dmScope ?? "main";
|
||||
const identityLinks = input.cfg.session?.identityLinks;
|
||||
|
||||
const choose = (agentId: string, matchedBy: ResolvedAgentRoute["matchedBy"]) => {
|
||||
const resolvedAgentId = pickFirstExistingAgentId(input.cfg, agentId);
|
||||
@@ -165,6 +168,7 @@ export function resolveAgentRoute(input: ResolveAgentRouteInput): ResolvedAgentR
|
||||
channel,
|
||||
peer,
|
||||
dmScope,
|
||||
identityLinks,
|
||||
}),
|
||||
mainSessionKey: buildAgentMainSessionKey({
|
||||
agentId: resolvedAgentId,
|
||||
|
||||
@@ -88,13 +88,23 @@ export function buildAgentPeerSessionKey(params: {
|
||||
channel: string;
|
||||
peerKind?: "dm" | "group" | "channel" | null;
|
||||
peerId?: string | null;
|
||||
identityLinks?: Record<string, string[]>;
|
||||
/** DM session scope. */
|
||||
dmScope?: "main" | "per-peer" | "per-channel-peer";
|
||||
}): string {
|
||||
const peerKind = params.peerKind ?? "dm";
|
||||
if (peerKind === "dm") {
|
||||
const dmScope = params.dmScope ?? "main";
|
||||
const peerId = (params.peerId ?? "").trim();
|
||||
let peerId = (params.peerId ?? "").trim();
|
||||
const linkedPeerId =
|
||||
dmScope === "main"
|
||||
? null
|
||||
: resolveLinkedPeerId({
|
||||
identityLinks: params.identityLinks,
|
||||
channel: params.channel,
|
||||
peerId,
|
||||
});
|
||||
if (linkedPeerId) peerId = linkedPeerId;
|
||||
if (dmScope === "per-channel-peer" && peerId) {
|
||||
const channel = (params.channel ?? "").trim().toLowerCase() || "unknown";
|
||||
return `agent:${normalizeAgentId(params.agentId)}:${channel}:dm:${peerId}`;
|
||||
@@ -112,6 +122,38 @@ export function buildAgentPeerSessionKey(params: {
|
||||
return `agent:${normalizeAgentId(params.agentId)}:${channel}:${peerKind}:${peerId}`;
|
||||
}
|
||||
|
||||
function resolveLinkedPeerId(params: {
|
||||
identityLinks?: Record<string, string[]>;
|
||||
channel: string;
|
||||
peerId: string;
|
||||
}): string | null {
|
||||
const identityLinks = params.identityLinks;
|
||||
if (!identityLinks) return null;
|
||||
const peerId = params.peerId.trim();
|
||||
if (!peerId) return null;
|
||||
const candidates = new Set<string>();
|
||||
const rawCandidate = normalizeToken(peerId);
|
||||
if (rawCandidate) candidates.add(rawCandidate);
|
||||
const channel = normalizeToken(params.channel);
|
||||
if (channel) {
|
||||
const scopedCandidate = normalizeToken(`${channel}:${peerId}`);
|
||||
if (scopedCandidate) candidates.add(scopedCandidate);
|
||||
}
|
||||
if (candidates.size === 0) return null;
|
||||
for (const [canonical, ids] of Object.entries(identityLinks)) {
|
||||
const canonicalName = canonical.trim();
|
||||
if (!canonicalName) continue;
|
||||
if (!Array.isArray(ids)) continue;
|
||||
for (const id of ids) {
|
||||
const normalized = normalizeToken(id);
|
||||
if (normalized && candidates.has(normalized)) {
|
||||
return canonicalName;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function buildGroupHistoryKey(params: {
|
||||
channel: string;
|
||||
accountId?: string | null;
|
||||
|
||||
Reference in New Issue
Block a user