refactor: prune legacy group prefixes
This commit is contained in:
@@ -534,7 +534,7 @@ async function processMessageWithPipeline(params: {
|
||||
Body: body,
|
||||
RawBody: rawBody,
|
||||
CommandBody: rawBody,
|
||||
From: isGroup ? `group:${chatId}` : `zalo:${senderId}`,
|
||||
From: isGroup ? `zalo:group:${chatId}` : `zalo:${senderId}`,
|
||||
To: `zalo:${chatId}`,
|
||||
SessionKey: route.sessionKey,
|
||||
AccountId: route.accountId,
|
||||
|
||||
@@ -195,9 +195,8 @@ async function processMessage(
|
||||
},
|
||||
});
|
||||
|
||||
const fromLabel = isGroup
|
||||
? `group:${chatId}`
|
||||
: senderName || `user:${senderId}`;
|
||||
const rawBody = content.trim();
|
||||
const fromLabel = isGroup ? `group:${chatId}` : senderName || `user:${senderId}`;
|
||||
const body = deps.formatAgentEnvelope({
|
||||
channel: "Zalo Personal",
|
||||
from: fromLabel,
|
||||
@@ -209,7 +208,7 @@ async function processMessage(
|
||||
Body: body,
|
||||
RawBody: rawBody,
|
||||
CommandBody: rawBody,
|
||||
From: isGroup ? `group:${chatId}` : `zalouser:${senderId}`,
|
||||
From: isGroup ? `zalouser:group:${chatId}` : `zalouser:${senderId}`,
|
||||
To: `zalouser:${chatId}`,
|
||||
SessionKey: route.sessionKey,
|
||||
AccountId: route.accountId,
|
||||
|
||||
@@ -309,7 +309,6 @@ export function createSessionStatusTool(opts?: {
|
||||
const isGroup =
|
||||
resolved.entry.chatType === "group" ||
|
||||
resolved.entry.chatType === "channel" ||
|
||||
resolved.key.startsWith("group:") ||
|
||||
resolved.key.includes(":group:") ||
|
||||
resolved.key.includes(":channel:");
|
||||
const groupActivation = isGroup
|
||||
|
||||
@@ -68,7 +68,7 @@ export function classifySessionKind(params: {
|
||||
if (key.startsWith("hook:")) return "hook";
|
||||
if (key.startsWith("node-") || key.startsWith("node:")) return "node";
|
||||
if (params.gatewayKind === "group") return "group";
|
||||
if (key.startsWith("group:") || key.includes(":group:") || key.includes(":channel:")) {
|
||||
if (key.includes(":group:") || key.includes(":channel:")) {
|
||||
return "group";
|
||||
}
|
||||
return "other";
|
||||
|
||||
@@ -23,11 +23,13 @@ export function resolveAnnounceTargetFromKey(sessionKey: string): AnnounceTarget
|
||||
if (!channelRaw) return null;
|
||||
const normalizedChannel = normalizeChannelId(channelRaw);
|
||||
const channel = normalizedChannel ?? channelRaw.toLowerCase();
|
||||
const kindTarget = normalizedChannel
|
||||
? kind === "channel"
|
||||
? `channel:${id}`
|
||||
: `group:${id}`
|
||||
: id;
|
||||
const kindTarget = (() => {
|
||||
if (!normalizedChannel) return id;
|
||||
if (normalizedChannel === "discord" || normalizedChannel === "slack") {
|
||||
return `channel:${id}`;
|
||||
}
|
||||
return kind === "channel" ? `channel:${id}` : `group:${id}`;
|
||||
})();
|
||||
const normalized = normalizedChannel
|
||||
? getChannelPlugin(normalizedChannel)?.messaging?.normalizeTarget?.(kindTarget)
|
||||
: undefined;
|
||||
|
||||
@@ -109,7 +109,7 @@ describe("group intro prompts", () => {
|
||||
await getReplyFromConfig(
|
||||
{
|
||||
Body: "status update",
|
||||
From: "group:dev",
|
||||
From: "discord:group:dev",
|
||||
To: "+1888",
|
||||
ChatType: "group",
|
||||
GroupSubject: "Release Squad",
|
||||
@@ -172,7 +172,7 @@ describe("group intro prompts", () => {
|
||||
await getReplyFromConfig(
|
||||
{
|
||||
Body: "ping",
|
||||
From: "group:tg",
|
||||
From: "telegram:group:tg",
|
||||
To: "+1777",
|
||||
ChatType: "group",
|
||||
GroupSubject: "Dev Chat",
|
||||
|
||||
@@ -211,7 +211,7 @@ describe("trigger handling", () => {
|
||||
const res = await getReplyFromConfig(
|
||||
{
|
||||
Body: "/elevated on",
|
||||
From: "group:123@g.us",
|
||||
From: "whatsapp:group:123@g.us",
|
||||
To: "whatsapp:+2000",
|
||||
Provider: "whatsapp",
|
||||
SenderE164: "+1000",
|
||||
|
||||
@@ -128,7 +128,7 @@ describe("trigger handling", () => {
|
||||
const res = await getReplyFromConfig(
|
||||
{
|
||||
Body: "/elevated off",
|
||||
From: "group:123@g.us",
|
||||
From: "whatsapp:group:123@g.us",
|
||||
To: "whatsapp:+2000",
|
||||
Provider: "whatsapp",
|
||||
SenderE164: "+1000",
|
||||
@@ -172,7 +172,7 @@ describe("trigger handling", () => {
|
||||
const res = await getReplyFromConfig(
|
||||
{
|
||||
Body: "/elevated on",
|
||||
From: "group:123@g.us",
|
||||
From: "whatsapp:group:123@g.us",
|
||||
To: "whatsapp:+2000",
|
||||
Provider: "whatsapp",
|
||||
SenderE164: "+1000",
|
||||
|
||||
@@ -135,7 +135,7 @@ describe("trigger handling", () => {
|
||||
|
||||
const ctx = {
|
||||
Body: "hi",
|
||||
From: "group:whatsapp:demo",
|
||||
From: "whatsapp:group:demo",
|
||||
To: "+2000",
|
||||
ChatType: "group" as const,
|
||||
Provider: "whatsapp" as const,
|
||||
|
||||
@@ -75,7 +75,7 @@ describe("buildThreadingToolContext", () => {
|
||||
const sessionCtx = {
|
||||
Provider: "imessage",
|
||||
ChatType: "group",
|
||||
From: "group:7",
|
||||
From: "imessage:group:7",
|
||||
To: "chat_id:7",
|
||||
} as TemplateContext;
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ describe("resolveGroupRequireMention", () => {
|
||||
};
|
||||
const ctx: TemplateContext = {
|
||||
Provider: "discord",
|
||||
From: "group:123",
|
||||
From: "discord:group:123",
|
||||
GroupChannel: "#general",
|
||||
GroupSpace: "145",
|
||||
};
|
||||
|
||||
@@ -6,6 +6,26 @@ import { isInternalMessageChannel } from "../../utils/message-channel.js";
|
||||
import { normalizeGroupActivation } from "../group-activation.js";
|
||||
import type { TemplateContext } from "../templating.js";
|
||||
|
||||
function extractGroupId(raw: string | undefined | null): string | undefined {
|
||||
const trimmed = (raw ?? "").trim();
|
||||
if (!trimmed) return undefined;
|
||||
const parts = trimmed.split(":").filter(Boolean);
|
||||
if (parts.length >= 3 && (parts[1] === "group" || parts[1] === "channel")) {
|
||||
return parts.slice(2).join(":") || undefined;
|
||||
}
|
||||
if (
|
||||
parts.length >= 2 &&
|
||||
parts[0]?.toLowerCase() === "whatsapp" &&
|
||||
trimmed.toLowerCase().includes("@g.us")
|
||||
) {
|
||||
return parts.slice(1).join(":") || undefined;
|
||||
}
|
||||
if (parts.length >= 2 && (parts[0] === "group" || parts[0] === "channel")) {
|
||||
return parts.slice(1).join(":") || undefined;
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
export function resolveGroupRequireMention(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
ctx: TemplateContext;
|
||||
@@ -15,7 +35,7 @@ export function resolveGroupRequireMention(params: {
|
||||
const rawChannel = groupResolution?.channel ?? ctx.Provider?.trim();
|
||||
const channel = normalizeChannelId(rawChannel);
|
||||
if (!channel) return true;
|
||||
const groupId = groupResolution?.id ?? ctx.From?.replace(/^group:/, "");
|
||||
const groupId = groupResolution?.id ?? extractGroupId(ctx.From);
|
||||
const groupChannel = ctx.GroupChannel?.trim() ?? ctx.GroupSubject?.trim();
|
||||
const groupSpace = ctx.GroupSpace?.trim();
|
||||
const requireMention = getChannelDock(channel)?.groups?.resolveRequireMention?.({
|
||||
@@ -61,7 +81,7 @@ export function buildGroupIntro(params: {
|
||||
activation === "always"
|
||||
? "Activation: always-on (you receive every group message)."
|
||||
: "Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included).";
|
||||
const groupId = params.sessionCtx.From?.replace(/^group:/, "");
|
||||
const groupId = params.sessionEntry?.groupId ?? extractGroupId(params.sessionCtx.From);
|
||||
const groupChannel = params.sessionCtx.GroupChannel?.trim() ?? subject;
|
||||
const groupSpace = params.sessionCtx.GroupSpace?.trim();
|
||||
const providerIdsLine = providerId
|
||||
|
||||
@@ -9,7 +9,7 @@ describe("finalizeInboundContext", () => {
|
||||
Body: "a\\nb\r\nc",
|
||||
RawBody: "raw\\nline",
|
||||
ChatType: "channel",
|
||||
From: "group:123@g.us",
|
||||
From: "whatsapp:group:123@g.us",
|
||||
GroupSubject: "Test",
|
||||
};
|
||||
|
||||
|
||||
@@ -227,6 +227,7 @@ export async function initSessionState(params: {
|
||||
displayName: baseEntry?.displayName,
|
||||
chatType: baseEntry?.chatType,
|
||||
channel: baseEntry?.channel,
|
||||
groupId: baseEntry?.groupId,
|
||||
subject: baseEntry?.subject,
|
||||
room: baseEntry?.room,
|
||||
space: baseEntry?.space,
|
||||
@@ -256,6 +257,7 @@ export async function initSessionState(params: {
|
||||
const nextSubject = nextRoom ? undefined : subject;
|
||||
sessionEntry.chatType = groupResolution.chatType ?? "group";
|
||||
sessionEntry.channel = channel;
|
||||
sessionEntry.groupId = groupResolution.id;
|
||||
if (nextSubject) sessionEntry.subject = nextSubject;
|
||||
if (nextRoom) sessionEntry.room = nextRoom;
|
||||
if (space) sessionEntry.space = space;
|
||||
|
||||
@@ -293,8 +293,7 @@ export function buildStatusMessage(args: StatusArgs): string {
|
||||
entry?.chatType === "group" ||
|
||||
entry?.chatType === "channel" ||
|
||||
Boolean(args.sessionKey?.includes(":group:")) ||
|
||||
Boolean(args.sessionKey?.includes(":channel:")) ||
|
||||
Boolean(args.sessionKey?.startsWith("group:"));
|
||||
Boolean(args.sessionKey?.includes(":channel:"));
|
||||
const groupActivationValue = isGroupSession
|
||||
? (args.groupActivation ?? entry?.groupActivation ?? "mention")
|
||||
: undefined;
|
||||
|
||||
@@ -13,10 +13,6 @@ export function normalizeSlackMessagingTarget(raw: string): string | undefined {
|
||||
const id = trimmed.slice(8).trim();
|
||||
return id ? `channel:${id}`.toLowerCase() : undefined;
|
||||
}
|
||||
if (trimmed.startsWith("group:")) {
|
||||
const id = trimmed.slice(6).trim();
|
||||
return id ? `channel:${id}`.toLowerCase() : undefined;
|
||||
}
|
||||
if (trimmed.startsWith("slack:")) {
|
||||
const id = trimmed.slice(6).trim();
|
||||
return id ? `user:${id}`.toLowerCase() : undefined;
|
||||
@@ -36,7 +32,7 @@ export function looksLikeSlackTargetId(raw: string): boolean {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return false;
|
||||
if (/^<@([A-Z0-9]+)>$/i.test(trimmed)) return true;
|
||||
if (/^(user|channel|group):/i.test(trimmed)) return true;
|
||||
if (/^(user|channel):/i.test(trimmed)) return true;
|
||||
if (/^slack:/i.test(trimmed)) return true;
|
||||
if (/^[@#]/.test(trimmed)) return true;
|
||||
return /^[CUWGD][A-Z0-9]{8,}$/i.test(trimmed);
|
||||
@@ -55,10 +51,6 @@ export function normalizeDiscordMessagingTarget(raw: string): string | undefined
|
||||
const id = trimmed.slice(8).trim();
|
||||
return id ? `channel:${id}`.toLowerCase() : undefined;
|
||||
}
|
||||
if (trimmed.startsWith("group:")) {
|
||||
const id = trimmed.slice(6).trim();
|
||||
return id ? `channel:${id}`.toLowerCase() : undefined;
|
||||
}
|
||||
if (trimmed.startsWith("discord:")) {
|
||||
const id = trimmed.slice(8).trim();
|
||||
return id ? `user:${id}`.toLowerCase() : undefined;
|
||||
@@ -74,7 +66,7 @@ export function looksLikeDiscordTargetId(raw: string): boolean {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return false;
|
||||
if (/^<@!?\d+>$/.test(trimmed)) return true;
|
||||
if (/^(user|channel|group|discord):/i.test(trimmed)) return true;
|
||||
if (/^(user|channel|discord):/i.test(trimmed)) return true;
|
||||
if (/^\d{6,}$/.test(trimmed)) return true;
|
||||
return false;
|
||||
}
|
||||
@@ -87,8 +79,6 @@ export function normalizeTelegramMessagingTarget(raw: string): string | undefine
|
||||
normalized = normalized.slice("telegram:".length).trim();
|
||||
} else if (normalized.startsWith("tg:")) {
|
||||
normalized = normalized.slice("tg:".length).trim();
|
||||
} else if (normalized.startsWith("group:")) {
|
||||
normalized = normalized.slice("group:".length).trim();
|
||||
}
|
||||
if (!normalized) return undefined;
|
||||
const tmeMatch =
|
||||
@@ -102,7 +92,7 @@ export function normalizeTelegramMessagingTarget(raw: string): string | undefine
|
||||
export function looksLikeTelegramTargetId(raw: string): boolean {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return false;
|
||||
if (/^(telegram|tg|group):/i.test(trimmed)) return true;
|
||||
if (/^(telegram|tg):/i.test(trimmed)) return true;
|
||||
if (trimmed.startsWith("@")) return true;
|
||||
return /^-?\d{6,}$/.test(trimmed);
|
||||
}
|
||||
|
||||
@@ -13,10 +13,7 @@ function getSessionRecipients(cfg: ClawdbotConfig) {
|
||||
const storePath = resolveStorePath(cfg.session?.store);
|
||||
const store = loadSessionStore(storePath);
|
||||
const isGroupKey = (key: string) =>
|
||||
key.startsWith("group:") ||
|
||||
key.includes(":group:") ||
|
||||
key.includes(":channel:") ||
|
||||
key.includes("@g.us");
|
||||
key.includes(":group:") || key.includes(":channel:") || key.includes("@g.us");
|
||||
const isCronKey = (key: string) => key.startsWith("cron:");
|
||||
|
||||
const recipients = Object.entries(store)
|
||||
|
||||
@@ -69,7 +69,7 @@ describe("doctor legacy state migrations", () => {
|
||||
) as Record<string, { sessionId: string }>;
|
||||
expect(store["agent:main:main"]?.sessionId).toBe("b");
|
||||
expect(store["agent:main:slack:channel:C123"]?.sessionId).toBe("c");
|
||||
expect(store["group:abc"]?.sessionId).toBe("d");
|
||||
expect(store["agent:main:unknown:group:abc"]?.sessionId).toBe("d");
|
||||
expect(store["agent:main:subagent:xyz"]?.sessionId).toBe("e");
|
||||
});
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ function classifyKey(key: string, entry?: SessionEntry): SessionRow["kind"] {
|
||||
if (entry?.chatType === "group" || entry?.chatType === "channel") {
|
||||
return "group";
|
||||
}
|
||||
if (key.startsWith("group:") || key.includes(":group:") || key.includes(":channel:")) {
|
||||
if (key.includes(":group:") || key.includes(":channel:")) {
|
||||
return "group";
|
||||
}
|
||||
return "direct";
|
||||
|
||||
@@ -22,7 +22,7 @@ const classifyKey = (key: string, entry?: SessionEntry): SessionStatus["kind"] =
|
||||
if (entry?.chatType === "group" || entry?.chatType === "channel") {
|
||||
return "group";
|
||||
}
|
||||
if (key.startsWith("group:") || key.includes(":group:") || key.includes(":channel:")) {
|
||||
if (key.includes(":group:") || key.includes(":channel:")) {
|
||||
return "group";
|
||||
}
|
||||
return "direct";
|
||||
|
||||
@@ -30,7 +30,9 @@ describe("sessions", () => {
|
||||
});
|
||||
|
||||
it("keeps group chats distinct", () => {
|
||||
expect(deriveSessionKey("per-sender", { From: "12345-678@g.us" })).toBe("group:12345-678@g.us");
|
||||
expect(deriveSessionKey("per-sender", { From: "12345-678@g.us" })).toBe(
|
||||
"whatsapp:group:12345-678@g.us",
|
||||
);
|
||||
});
|
||||
|
||||
it("prefixes group keys with provider when available", () => {
|
||||
@@ -45,7 +47,7 @@ describe("sessions", () => {
|
||||
|
||||
it("keeps explicit provider when provided in group key", () => {
|
||||
expect(
|
||||
resolveSessionKey("per-sender", { From: "group:discord:12345", ChatType: "group" }, "main"),
|
||||
resolveSessionKey("per-sender", { From: "discord:group:12345", ChatType: "group" }, "main"),
|
||||
).toBe("agent:main:discord:group:12345");
|
||||
});
|
||||
|
||||
@@ -87,7 +89,7 @@ describe("sessions", () => {
|
||||
|
||||
it("leaves groups untouched even with main key", () => {
|
||||
expect(resolveSessionKey("per-sender", { From: "12345-678@g.us" }, "main")).toBe(
|
||||
"agent:main:group:12345-678@g.us",
|
||||
"agent:main:whatsapp:group:12345-678@g.us",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ export function buildGroupDisplayName(params: {
|
||||
(room && space
|
||||
? `${space}${room.startsWith("#") ? "" : "#"}${room}`
|
||||
: room || subject || space || "") || "";
|
||||
const fallbackId = params.id?.trim() || params.key.replace(/^group:/, "");
|
||||
const fallbackId = params.id?.trim() || params.key;
|
||||
const rawLabel = detail || fallbackId;
|
||||
let token = normalizeGroupLabel(rawLabel);
|
||||
if (!token) {
|
||||
@@ -52,84 +52,49 @@ export function buildGroupDisplayName(params: {
|
||||
|
||||
export function resolveGroupSessionKey(ctx: MsgContext): GroupKeyResolution | null {
|
||||
const from = typeof ctx.From === "string" ? ctx.From.trim() : "";
|
||||
if (!from) return null;
|
||||
const chatType = ctx.ChatType?.trim().toLowerCase();
|
||||
const isGroup =
|
||||
chatType === "group" ||
|
||||
from.startsWith("group:") ||
|
||||
from.includes("@g.us") ||
|
||||
const normalizedChatType =
|
||||
chatType === "channel" ? "channel" : chatType === "group" ? "group" : undefined;
|
||||
|
||||
const isWhatsAppGroupId = from.toLowerCase().endsWith("@g.us");
|
||||
const looksLikeGroup =
|
||||
normalizedChatType === "group" ||
|
||||
normalizedChatType === "channel" ||
|
||||
from.includes(":group:") ||
|
||||
from.includes(":channel:");
|
||||
if (!isGroup) return null;
|
||||
from.includes(":channel:") ||
|
||||
isWhatsAppGroupId;
|
||||
if (!looksLikeGroup) return null;
|
||||
|
||||
const providerHint = ctx.Provider?.trim().toLowerCase();
|
||||
const hasGroupPrefix = from.startsWith("group:");
|
||||
const raw = (hasGroupPrefix ? from.slice("group:".length) : from).trim();
|
||||
|
||||
let provider: string | undefined;
|
||||
let kind: "group" | "channel" | undefined;
|
||||
let id = "";
|
||||
const parts = from.split(":").filter(Boolean);
|
||||
const head = parts[0]?.trim().toLowerCase() ?? "";
|
||||
const headIsSurface = head ? getGroupSurfaces().has(head) : false;
|
||||
|
||||
const parseKind = (value: string) => {
|
||||
if (value === "channel") return "channel";
|
||||
return "group";
|
||||
};
|
||||
const provider = headIsSurface
|
||||
? head
|
||||
: (providerHint ?? (isWhatsAppGroupId ? "whatsapp" : undefined));
|
||||
if (!provider) return null;
|
||||
|
||||
const parseParts = (parts: string[]) => {
|
||||
if (parts.length >= 2 && getGroupSurfaces().has(parts[0])) {
|
||||
provider = parts[0];
|
||||
if (parts.length >= 3) {
|
||||
const kindCandidate = parts[1];
|
||||
if (["group", "channel"].includes(kindCandidate)) {
|
||||
kind = parseKind(kindCandidate);
|
||||
id = parts.slice(2).join(":");
|
||||
} else {
|
||||
id = parts.slice(1).join(":");
|
||||
}
|
||||
} else {
|
||||
id = parts[1];
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (parts.length >= 2 && ["group", "channel"].includes(parts[0])) {
|
||||
kind = parseKind(parts[0]);
|
||||
id = parts.slice(1).join(":");
|
||||
}
|
||||
};
|
||||
|
||||
if (hasGroupPrefix) {
|
||||
const legacyParts = raw.split(":").filter(Boolean);
|
||||
if (legacyParts.length > 1) {
|
||||
parseParts(legacyParts);
|
||||
} else {
|
||||
id = raw;
|
||||
}
|
||||
} else if (from.includes("@g.us") && !from.includes(":")) {
|
||||
id = from;
|
||||
} else {
|
||||
parseParts(from.split(":").filter(Boolean));
|
||||
if (!id) {
|
||||
id = raw || from;
|
||||
}
|
||||
}
|
||||
|
||||
const resolvedProvider = provider ?? providerHint;
|
||||
if (!resolvedProvider) {
|
||||
const legacy = hasGroupPrefix ? `group:${raw}` : `group:${from}`;
|
||||
return {
|
||||
key: legacy,
|
||||
id: raw || from,
|
||||
chatType: "group",
|
||||
};
|
||||
}
|
||||
|
||||
const resolvedKind = kind === "channel" ? "channel" : "group";
|
||||
const key = `${resolvedProvider}:${resolvedKind}:${id || raw || from}`;
|
||||
const second = parts[1]?.trim().toLowerCase();
|
||||
const secondIsKind = second === "group" || second === "channel";
|
||||
const kind = secondIsKind
|
||||
? (second as "group" | "channel")
|
||||
: from.includes(":channel:") || normalizedChatType === "channel"
|
||||
? "channel"
|
||||
: "group";
|
||||
const id = headIsSurface
|
||||
? secondIsKind
|
||||
? parts.slice(2).join(":")
|
||||
: parts.slice(1).join(":")
|
||||
: from;
|
||||
const finalId = id.trim();
|
||||
if (!finalId) return null;
|
||||
|
||||
return {
|
||||
key,
|
||||
channel: resolvedProvider,
|
||||
id: id || raw || from,
|
||||
chatType: resolvedKind === "channel" ? "channel" : "group",
|
||||
key: `${provider}:${kind}:${finalId}`,
|
||||
channel: provider,
|
||||
id: finalId,
|
||||
chatType: kind === "channel" ? "channel" : "group",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ export function resolveSessionKey(scope: SessionScope, ctx: MsgContext, mainKey?
|
||||
agentId: DEFAULT_AGENT_ID,
|
||||
mainKey: canonicalMainKey,
|
||||
});
|
||||
const isGroup = raw.startsWith("group:") || raw.includes(":group:") || raw.includes(":channel:");
|
||||
const isGroup = raw.includes(":group:") || raw.includes(":channel:");
|
||||
if (!isGroup) return canonical;
|
||||
return `agent:${DEFAULT_AGENT_ID}:${raw}`;
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ export type SessionEntry = {
|
||||
label?: string;
|
||||
displayName?: string;
|
||||
channel?: string;
|
||||
groupId?: string;
|
||||
subject?: string;
|
||||
room?: string;
|
||||
space?: string;
|
||||
|
||||
@@ -223,7 +223,7 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
|
||||
|
||||
const effectiveFrom = isDirectMessage
|
||||
? `discord:${author.id}`
|
||||
: (autoThreadContext?.From ?? `group:${message.channelId}`);
|
||||
: (autoThreadContext?.From ?? `discord:channel:${message.channelId}`);
|
||||
const effectiveTo = autoThreadContext?.To ?? replyTarget;
|
||||
if (!effectiveTo) {
|
||||
runtime.error?.(danger("discord: missing reply target"));
|
||||
|
||||
@@ -601,7 +601,11 @@ async function dispatchDiscordCommandInteraction(params: {
|
||||
RawBody: prompt,
|
||||
CommandBody: prompt,
|
||||
CommandArgs: commandArgs,
|
||||
From: isDirectMessage ? `discord:${user.id}` : `group:${channelId}`,
|
||||
From: isDirectMessage
|
||||
? `discord:${user.id}`
|
||||
: isGroupDm
|
||||
? `discord:group:${channelId}`
|
||||
: `discord:channel:${channelId}`,
|
||||
To: `slash:${user.id}`,
|
||||
SessionKey: `agent:${route.agentId}:${sessionPrefix}:${user.id}`,
|
||||
CommandTargetSessionKey: route.sessionKey,
|
||||
|
||||
@@ -28,7 +28,7 @@ describe("resolveDiscordAutoThreadContext", () => {
|
||||
});
|
||||
expect(context).not.toBeNull();
|
||||
expect(context?.To).toBe("channel:thread");
|
||||
expect(context?.From).toBe("group:thread");
|
||||
expect(context?.From).toBe("discord:channel:thread");
|
||||
expect(context?.OriginatingTo).toBe("channel:thread");
|
||||
expect(context?.SessionKey).toBe(
|
||||
buildAgentSessionKey({
|
||||
|
||||
@@ -194,7 +194,7 @@ export function resolveDiscordAutoThreadContext(params: {
|
||||
|
||||
return {
|
||||
createdThreadId,
|
||||
From: `group:${createdThreadId}`,
|
||||
From: `${params.channel}:channel:${createdThreadId}`,
|
||||
To: `channel:${createdThreadId}`,
|
||||
OriginatingTo: `channel:${createdThreadId}`,
|
||||
SessionKey: threadSessionKey,
|
||||
|
||||
@@ -17,20 +17,23 @@ describe("gateway session utils", () => {
|
||||
expect(res.items).toEqual(["b", "c"]);
|
||||
});
|
||||
|
||||
test("parseGroupKey handles group prefixes", () => {
|
||||
expect(parseGroupKey("group:abc")).toEqual({ id: "abc" });
|
||||
test("parseGroupKey handles group keys", () => {
|
||||
expect(parseGroupKey("discord:group:dev")).toEqual({
|
||||
channel: "discord",
|
||||
kind: "group",
|
||||
id: "dev",
|
||||
});
|
||||
expect(parseGroupKey("agent:ops:discord:group:dev")).toEqual({
|
||||
channel: "discord",
|
||||
kind: "group",
|
||||
id: "dev",
|
||||
});
|
||||
expect(parseGroupKey("foo:bar")).toBeNull();
|
||||
});
|
||||
|
||||
test("classifySessionKey respects chat type + prefixes", () => {
|
||||
expect(classifySessionKey("global")).toBe("global");
|
||||
expect(classifySessionKey("unknown")).toBe("unknown");
|
||||
expect(classifySessionKey("group:abc")).toBe("group");
|
||||
expect(classifySessionKey("discord:group:dev")).toBe("group");
|
||||
expect(classifySessionKey("main")).toBe("direct");
|
||||
const entry = { chatType: "group" } as SessionEntry;
|
||||
@@ -52,7 +55,9 @@ describe("gateway session utils", () => {
|
||||
session: { mainKey: "main" },
|
||||
agents: { list: [{ id: "ops", default: true }] },
|
||||
} as ClawdbotConfig;
|
||||
expect(resolveSessionStoreKey({ cfg, sessionKey: "group:123" })).toBe("agent:ops:group:123");
|
||||
expect(resolveSessionStoreKey({ cfg, sessionKey: "discord:group:123" })).toBe(
|
||||
"agent:ops:discord:group:123",
|
||||
);
|
||||
expect(resolveSessionStoreKey({ cfg, sessionKey: "agent:alpha:main" })).toBe(
|
||||
"agent:alpha:main",
|
||||
);
|
||||
|
||||
@@ -60,7 +60,7 @@ export function classifySessionKey(key: string, entry?: SessionEntry): GatewaySe
|
||||
if (entry?.chatType === "group" || entry?.chatType === "channel") {
|
||||
return "group";
|
||||
}
|
||||
if (key.startsWith("group:") || key.includes(":group:") || key.includes(":channel:")) {
|
||||
if (key.includes(":group:") || key.includes(":channel:")) {
|
||||
return "group";
|
||||
}
|
||||
return "direct";
|
||||
@@ -71,10 +71,6 @@ export function parseGroupKey(
|
||||
): { channel?: string; kind?: "group" | "channel"; id?: string } | null {
|
||||
const agentParsed = parseAgentSessionKey(key);
|
||||
const rawKey = agentParsed?.rest ?? key;
|
||||
if (rawKey.startsWith("group:")) {
|
||||
const raw = rawKey.slice("group:".length);
|
||||
return raw ? { id: raw } : null;
|
||||
}
|
||||
const parts = rawKey.split(":").filter(Boolean);
|
||||
if (parts.length >= 3) {
|
||||
const [channel, kind, ...rest] = parts;
|
||||
|
||||
@@ -422,7 +422,7 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
|
||||
Body: combinedBody,
|
||||
RawBody: bodyText,
|
||||
CommandBody: bodyText,
|
||||
From: isGroup ? `group:${chatId}` : `imessage:${sender}`,
|
||||
From: isGroup ? `imessage:group:${chatId ?? "unknown"}` : `imessage:${sender}`,
|
||||
To: imessageTo,
|
||||
SessionKey: route.sessionKey,
|
||||
AccountId: route.accountId,
|
||||
|
||||
@@ -13,8 +13,8 @@ describe("imessage targets", () => {
|
||||
expect(target).toEqual({ kind: "chat_id", chatId: 123 });
|
||||
});
|
||||
|
||||
it("parses group chat targets", () => {
|
||||
const target = parseIMessageTarget("group:456");
|
||||
it("parses chat targets", () => {
|
||||
const target = parseIMessageTarget("chat:456");
|
||||
expect(target).toEqual({ kind: "chat_id", chatId: 456 });
|
||||
});
|
||||
|
||||
|
||||
@@ -53,8 +53,7 @@ export function parseIMessageTarget(raw: string): IMessageTarget {
|
||||
const isChatTarget =
|
||||
CHAT_ID_PREFIXES.some((p) => remainderLower.startsWith(p)) ||
|
||||
CHAT_GUID_PREFIXES.some((p) => remainderLower.startsWith(p)) ||
|
||||
CHAT_IDENTIFIER_PREFIXES.some((p) => remainderLower.startsWith(p)) ||
|
||||
remainderLower.startsWith("group:");
|
||||
CHAT_IDENTIFIER_PREFIXES.some((p) => remainderLower.startsWith(p));
|
||||
if (isChatTarget) {
|
||||
return parseIMessageTarget(remainder);
|
||||
}
|
||||
@@ -89,16 +88,6 @@ export function parseIMessageTarget(raw: string): IMessageTarget {
|
||||
}
|
||||
}
|
||||
|
||||
if (lower.startsWith("group:")) {
|
||||
const value = stripPrefix(trimmed, "group:");
|
||||
const chatId = Number.parseInt(value, 10);
|
||||
if (Number.isFinite(chatId)) {
|
||||
return { kind: "chat_id", chatId };
|
||||
}
|
||||
if (!value) throw new Error("group target is required");
|
||||
return { kind: "chat_guid", chatGuid: value };
|
||||
}
|
||||
|
||||
return { kind: "handle", to: trimmed, service: "auto" };
|
||||
}
|
||||
|
||||
@@ -137,13 +126,6 @@ export function parseIMessageAllowTarget(raw: string): IMessageAllowTarget {
|
||||
}
|
||||
}
|
||||
|
||||
if (lower.startsWith("group:")) {
|
||||
const value = stripPrefix(trimmed, "group:");
|
||||
const chatId = Number.parseInt(value, 10);
|
||||
if (Number.isFinite(chatId)) return { kind: "chat_id", chatId };
|
||||
if (value) return { kind: "chat_guid", chatGuid: value };
|
||||
}
|
||||
|
||||
return { kind: "handle", handle: normalizeIMessageHandle(trimmed) };
|
||||
}
|
||||
|
||||
|
||||
@@ -220,7 +220,7 @@ describe("resolveHeartbeatDeliveryTarget", () => {
|
||||
const entry = {
|
||||
...baseEntry,
|
||||
lastChannel: "whatsapp" as const,
|
||||
lastTo: "whatsapp:group:120363401234567890@G.US",
|
||||
lastTo: "whatsapp:120363401234567890@G.US",
|
||||
};
|
||||
expect(resolveHeartbeatDeliveryTarget({ cfg, entry })).toEqual({
|
||||
channel: "whatsapp",
|
||||
|
||||
@@ -106,7 +106,7 @@ describe("runMessageAction context isolation", () => {
|
||||
action: "send",
|
||||
params: {
|
||||
channel: "whatsapp",
|
||||
target: "group:123@g.us",
|
||||
target: "123@g.us",
|
||||
message: "hi",
|
||||
},
|
||||
toolContext: { currentChannelId: "123@g.us" },
|
||||
|
||||
@@ -32,7 +32,7 @@ describe("resolveOutboundTarget", () => {
|
||||
name: "normalizes prefixed/uppercase whatsapp group targets",
|
||||
input: {
|
||||
channel: "whatsapp" as const,
|
||||
to: " WhatsApp:Group:120363401234567890@G.US ",
|
||||
to: " WhatsApp:120363401234567890@G.US ",
|
||||
},
|
||||
expected: { ok: true as const, to: "120363401234567890@g.us" },
|
||||
},
|
||||
|
||||
@@ -61,7 +61,15 @@ function isSurfaceGroupKey(key: string): boolean {
|
||||
}
|
||||
|
||||
function isLegacyGroupKey(key: string): boolean {
|
||||
return key.startsWith("group:") || key.includes("@g.us");
|
||||
const trimmed = key.trim();
|
||||
if (!trimmed) return false;
|
||||
if (trimmed.startsWith("group:")) return true;
|
||||
const lower = trimmed.toLowerCase();
|
||||
if (!lower.includes("@g.us")) return false;
|
||||
// Legacy WhatsApp group keys: bare JID or "whatsapp:<jid>" without explicit ":group:" kind.
|
||||
if (!trimmed.includes(":")) return true;
|
||||
if (lower.startsWith("whatsapp:") && !trimmed.includes(":group:")) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function normalizeSessionKeyForAgent(key: string, agentId: string): string {
|
||||
@@ -72,6 +80,22 @@ function normalizeSessionKeyForAgent(key: string, agentId: string): string {
|
||||
const rest = raw.slice("subagent:".length);
|
||||
return `agent:${normalizeAgentId(agentId)}:subagent:${rest}`;
|
||||
}
|
||||
if (raw.startsWith("group:")) {
|
||||
const id = raw.slice("group:".length).trim();
|
||||
if (!id) return raw;
|
||||
const channel = id.toLowerCase().includes("@g.us") ? "whatsapp" : "unknown";
|
||||
return `agent:${normalizeAgentId(agentId)}:${channel}:group:${id}`;
|
||||
}
|
||||
if (!raw.includes(":") && raw.toLowerCase().includes("@g.us")) {
|
||||
return `agent:${normalizeAgentId(agentId)}:whatsapp:group:${raw}`;
|
||||
}
|
||||
if (raw.toLowerCase().startsWith("whatsapp:") && raw.toLowerCase().includes("@g.us")) {
|
||||
const remainder = raw.slice("whatsapp:".length).trim();
|
||||
const cleaned = remainder.replace(/^group:/i, "").trim();
|
||||
if (cleaned && !isSurfaceGroupKey(raw)) {
|
||||
return `agent:${normalizeAgentId(agentId)}:whatsapp:group:${cleaned}`;
|
||||
}
|
||||
}
|
||||
if (isSurfaceGroupKey(raw)) {
|
||||
return `agent:${normalizeAgentId(agentId)}:${raw}`;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ function deriveChannelFromKey(key?: string) {
|
||||
|
||||
function deriveChatTypeFromKey(key?: string): SessionChatType | undefined {
|
||||
if (!key) return undefined;
|
||||
if (key.startsWith("group:") || key.includes(":group:")) return "group";
|
||||
if (key.includes(":group:")) return "group";
|
||||
if (key.includes(":channel:")) return "channel";
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -269,7 +269,7 @@ describe("createTelegramBot", () => {
|
||||
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||
const payload = replySpy.mock.calls[0][0];
|
||||
expect(payload.SessionKey).toContain("telegram:group:-1001234567890:topic:99");
|
||||
expect(payload.From).toBe("group:-1001234567890:topic:99");
|
||||
expect(payload.From).toBe("telegram:group:-1001234567890:topic:99");
|
||||
expect(payload.MessageThreadId).toBe(99);
|
||||
expect(payload.IsForum).toBe(true);
|
||||
expect(sendChatActionSpy).toHaveBeenCalledWith(-1001234567890, "typing", {
|
||||
|
||||
@@ -1815,7 +1815,7 @@ describe("createTelegramBot", () => {
|
||||
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||
const payload = replySpy.mock.calls[0][0];
|
||||
expect(payload.SessionKey).toContain("telegram:group:-1001234567890:topic:99");
|
||||
expect(payload.From).toBe("group:-1001234567890:topic:99");
|
||||
expect(payload.From).toBe("telegram:group:-1001234567890:topic:99");
|
||||
expect(payload.MessageThreadId).toBe(99);
|
||||
expect(payload.IsForum).toBe(true);
|
||||
expect(sendChatActionSpy).toHaveBeenCalledWith(-1001234567890, "typing", {
|
||||
|
||||
@@ -59,7 +59,7 @@ export function buildTelegramGroupPeerId(chatId: number | string, messageThreadI
|
||||
}
|
||||
|
||||
export function buildTelegramGroupFrom(chatId: number | string, messageThreadId?: number) {
|
||||
return messageThreadId != null ? `group:${chatId}:topic:${messageThreadId}` : `group:${chatId}`;
|
||||
return `telegram:group:${buildTelegramGroupPeerId(chatId, messageThreadId)}`;
|
||||
}
|
||||
|
||||
export function buildSenderName(msg: TelegramMessage) {
|
||||
|
||||
@@ -41,7 +41,7 @@ describe("applyGroupGating", () => {
|
||||
sendMedia: async () => {},
|
||||
},
|
||||
conversationId: "123@g.us",
|
||||
groupHistoryKey: "group:123@g.us",
|
||||
groupHistoryKey: "whatsapp:default:group:123@g.us",
|
||||
agentId: "main",
|
||||
sessionKey: "agent:main:whatsapp:group:123@g.us",
|
||||
baseMentionConfig: { mentionRegexes: [] },
|
||||
|
||||
@@ -9,15 +9,6 @@ describe("normalizeWhatsAppTarget", () => {
|
||||
expect(normalizeWhatsAppTarget("whatsapp:120363401234567890@g.us")).toBe(
|
||||
"120363401234567890@g.us",
|
||||
);
|
||||
expect(normalizeWhatsAppTarget("whatsapp:group:120363401234567890@g.us")).toBe(
|
||||
"120363401234567890@g.us",
|
||||
);
|
||||
expect(normalizeWhatsAppTarget("group:123456789-987654321@g.us")).toBe(
|
||||
"123456789-987654321@g.us",
|
||||
);
|
||||
expect(normalizeWhatsAppTarget(" WhatsApp:Group:123456789-987654321@G.US ")).toBe(
|
||||
"123456789-987654321@g.us",
|
||||
);
|
||||
});
|
||||
|
||||
it("normalizes direct JIDs to E.164", () => {
|
||||
@@ -43,12 +34,15 @@ describe("normalizeWhatsAppTarget", () => {
|
||||
expect(normalizeWhatsAppTarget("whatsapp:")).toBeNull();
|
||||
expect(normalizeWhatsAppTarget("@g.us")).toBeNull();
|
||||
expect(normalizeWhatsAppTarget("whatsapp:group:@g.us")).toBeNull();
|
||||
expect(normalizeWhatsAppTarget("whatsapp:group:120363401234567890@g.us")).toBeNull();
|
||||
expect(normalizeWhatsAppTarget("group:123456789-987654321@g.us")).toBeNull();
|
||||
expect(normalizeWhatsAppTarget(" WhatsApp:Group:123456789-987654321@G.US ")).toBeNull();
|
||||
expect(normalizeWhatsAppTarget("abc@s.whatsapp.net")).toBeNull();
|
||||
});
|
||||
|
||||
it("handles repeated prefixes", () => {
|
||||
expect(normalizeWhatsAppTarget("whatsapp:whatsapp:+1555")).toBe("+1555");
|
||||
expect(normalizeWhatsAppTarget("group:group:120@g.us")).toBe("120@g.us");
|
||||
expect(normalizeWhatsAppTarget("group:group:120@g.us")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -70,7 +64,7 @@ describe("isWhatsAppGroupJid", () => {
|
||||
expect(isWhatsAppGroupJid("120363401234567890@g.us")).toBe(true);
|
||||
expect(isWhatsAppGroupJid("123456789-987654321@g.us")).toBe(true);
|
||||
expect(isWhatsAppGroupJid("whatsapp:120363401234567890@g.us")).toBe(true);
|
||||
expect(isWhatsAppGroupJid("whatsapp:group:120363401234567890@g.us")).toBe(true);
|
||||
expect(isWhatsAppGroupJid("whatsapp:group:120363401234567890@g.us")).toBe(false);
|
||||
expect(isWhatsAppGroupJid("x@g.us")).toBe(false);
|
||||
expect(isWhatsAppGroupJid("@g.us")).toBe(false);
|
||||
expect(isWhatsAppGroupJid("120@g.usx")).toBe(false);
|
||||
|
||||
@@ -7,10 +7,7 @@ function stripWhatsAppTargetPrefixes(value: string): string {
|
||||
let candidate = value.trim();
|
||||
for (;;) {
|
||||
const before = candidate;
|
||||
candidate = candidate
|
||||
.replace(/^whatsapp:/i, "")
|
||||
.replace(/^group:/i, "")
|
||||
.trim();
|
||||
candidate = candidate.replace(/^whatsapp:/i, "").trim();
|
||||
if (candidate === before) return candidate;
|
||||
}
|
||||
}
|
||||
@@ -59,6 +56,9 @@ export function normalizeWhatsAppTarget(value: string): string | null {
|
||||
const normalized = normalizeE164(phone);
|
||||
return normalized.length > 1 ? normalized : null;
|
||||
}
|
||||
// If the caller passed a JID-ish string that we don't understand, fail fast.
|
||||
// Otherwise normalizeE164 would happily treat "group:120@g.us" as a phone number.
|
||||
if (candidate.includes("@")) return null;
|
||||
const normalized = normalizeE164(candidate);
|
||||
return normalized.length > 1 ? normalized : null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user