
























@@ -59,6 +59,7 @@ async function resolveDispatchDecision(params: {
5959groupHistories?: Parameters<typeof resolveIMessageInboundDecision>[0]["groupHistories"];
6060allowFrom?: string[];
6161groupAllowFrom?: string[];
62+allowLegacyConversationAllowFromForGroup?: boolean;
6263groupPolicy?: "open" | "allowlist" | "disabled";
6364dmPolicy?: "open" | "pairing" | "allowlist" | "disabled";
6465}) {
@@ -72,6 +73,7 @@ async function resolveDispatchDecision(params: {
7273bodyText: params.message.text ?? "",
7374allowFrom: params.allowFrom ?? ["*"],
7475groupAllowFrom: params.groupAllowFrom ?? [],
76+allowLegacyConversationAllowFromForGroup: params.allowLegacyConversationAllowFromForGroup,
7577groupPolicy: params.groupPolicy ?? "open",
7678dmPolicy: params.dmPolicy ?? "open",
7779storeAllowFrom: [],
@@ -253,6 +255,90 @@ describe("imessage monitor gating + envelope builders", () => {
253255expect(ctxPayload.Body ?? "").not.toContain("[Replying to");
254256});
255257258+it("keeps group reply context when the group allowlist matches the chat target", async () => {
259+const cfg = baseCfg();
260+cfg.channels ??= {};
261+cfg.channels.imessage ??= {};
262+cfg.channels.imessage.groupPolicy = "allowlist";
263+cfg.channels.imessage.contextVisibility = "allowlist";
264+265+const message: IMessagePayload = {
266+id: 8,
267+chat_id: 55,
268+sender: "+15550001111",
269+is_from_me: false,
270+text: "@openclaw replying now",
271+is_group: true,
272+reply_to_id: 9001,
273+reply_to_text: "quoted context",
274+reply_to_sender: "+15559998888",
275+};
276+const { decision, groupHistories } = await resolveDispatchDecision({
277+ cfg,
278+ message,
279+allowFrom: ["*"],
280+groupAllowFrom: ["chat_id:55"],
281+groupPolicy: "allowlist",
282+});
283+const { ctxPayload } = buildIMessageInboundContext({
284+ cfg,
285+ decision,
286+ message,
287+historyLimit: 0,
288+ groupHistories,
289+});
290+291+expect(ctxPayload.ReplyToId).toBe("9001");
292+expect(ctxPayload.ReplyToBody).toBe("quoted context");
293+expect(ctxPayload.ReplyToSender).toBe("+15559998888");
294+expect(ctxPayload.Body ?? "").toContain("[Replying to +15559998888 id:9001]");
295+});
296+297+it("keeps group reply context when the group allowlist matches an access group", async () => {
298+const cfg = baseCfg();
299+cfg.channels ??= {};
300+cfg.channels.imessage ??= {};
301+cfg.channels.imessage.groupPolicy = "allowlist";
302+cfg.channels.imessage.contextVisibility = "allowlist";
303+cfg.accessGroups = {
304+oncall: {
305+type: "message.senders",
306+members: { imessage: ["+15559998888"] },
307+},
308+};
309+310+const message: IMessagePayload = {
311+id: 9,
312+chat_id: 56,
313+sender: "+15559998888",
314+is_from_me: false,
315+text: "@openclaw replying now",
316+is_group: true,
317+reply_to_id: 9002,
318+reply_to_text: "own quoted context",
319+reply_to_sender: "+15559998888",
320+};
321+const { decision, groupHistories } = await resolveDispatchDecision({
322+ cfg,
323+ message,
324+allowFrom: ["*"],
325+groupAllowFrom: ["accessGroup:oncall"],
326+groupPolicy: "allowlist",
327+});
328+const { ctxPayload } = buildIMessageInboundContext({
329+ cfg,
330+ decision,
331+ message,
332+historyLimit: 0,
333+ groupHistories,
334+});
335+336+expect(ctxPayload.ReplyToId).toBe("9002");
337+expect(ctxPayload.ReplyToBody).toBe("own quoted context");
338+expect(ctxPayload.ReplyToSender).toBe("+15559998888");
339+expect(ctxPayload.Body ?? "").toContain("[Replying to +15559998888 id:9002]");
340+});
341+256342it("keeps group reply context in allowlist_quote mode", async () => {
257343const cfg = baseCfg();
258344cfg.channels ??= {};
@@ -432,6 +518,163 @@ describe("imessage monitor gating + envelope builders", () => {
432518expect(allowed.kind).toBe("dispatch");
433519});
434520521+it("uses legacy conversation allowFrom entries for group admission", async () => {
522+const cfg = baseCfg();
523+cfg.channels ??= {};
524+cfg.channels.imessage ??= {};
525+cfg.channels.imessage.groupPolicy = "allowlist";
526+527+const { decision } = await resolveDispatchDecision({
528+ cfg,
529+message: {
530+id: 35,
531+chat_id: 101,
532+sender: "+15550003333",
533+is_from_me: false,
534+text: "@openclaw ok",
535+is_group: true,
536+},
537+allowFrom: ["chat_id:101"],
538+groupAllowFrom: [],
539+allowLegacyConversationAllowFromForGroup: true,
540+groupPolicy: "allowlist",
541+});
542+543+expect(decision.kind).toBe("dispatch");
544+});
545+546+it("does not use legacy conversation allowFrom entries when groupAllowFrom is explicitly empty", async () => {
547+const cfg = baseCfg();
548+cfg.channels ??= {};
549+cfg.channels.imessage ??= {};
550+cfg.channels.imessage.groupPolicy = "allowlist";
551+552+const decision = await resolveIMessageInboundDecision({
553+ cfg,
554+accountId: "default",
555+message: {
556+id: 38,
557+chat_id: 101,
558+sender: "+15550003333",
559+is_from_me: false,
560+text: "@openclaw ok",
561+is_group: true,
562+},
563+opts: {},
564+messageText: "@openclaw ok",
565+bodyText: "@openclaw ok",
566+allowFrom: ["chat_id:101"],
567+groupAllowFrom: [],
568+groupPolicy: "allowlist",
569+dmPolicy: "pairing",
570+storeAllowFrom: [],
571+historyLimit: 0,
572+groupHistories: new Map(),
573+});
574+575+expect(decision).toEqual({
576+kind: "drop",
577+reason: "groupPolicy allowlist (empty groupAllowFrom)",
578+});
579+});
580+581+it("does not merge legacy conversation allowFrom entries when groupAllowFrom is configured", async () => {
582+const cfg = baseCfg();
583+cfg.channels ??= {};
584+cfg.channels.imessage ??= {};
585+cfg.channels.imessage.groupPolicy = "allowlist";
586+587+const decision = await resolveIMessageInboundDecision({
588+ cfg,
589+accountId: "default",
590+message: {
591+id: 37,
592+chat_id: 101,
593+sender: "+15550003333",
594+is_from_me: false,
595+text: "@openclaw ok",
596+is_group: true,
597+},
598+opts: {},
599+messageText: "@openclaw ok",
600+bodyText: "@openclaw ok",
601+allowFrom: ["chat_id:101"],
602+groupAllowFrom: ["+15550004444"],
603+groupPolicy: "allowlist",
604+dmPolicy: "pairing",
605+storeAllowFrom: [],
606+historyLimit: 0,
607+groupHistories: new Map(),
608+});
609+610+expect(decision).toEqual({ kind: "drop", reason: "not in groupAllowFrom" });
611+});
612+613+it("does not authorize group control commands from conversation allowlist entries", async () => {
614+const cfg = baseCfg();
615+cfg.channels ??= {};
616+cfg.channels.imessage ??= {};
617+cfg.channels.imessage.groupPolicy = "allowlist";
618+619+const decision = await resolveIMessageInboundDecision({
620+ cfg,
621+accountId: "default",
622+message: {
623+id: 34,
624+chat_id: 101,
625+sender: "+15550003333",
626+is_from_me: false,
627+text: "/status",
628+is_group: true,
629+},
630+opts: {},
631+messageText: "/status",
632+bodyText: "/status",
633+allowFrom: [],
634+groupAllowFrom: ["chat_id:101"],
635+groupPolicy: "allowlist",
636+dmPolicy: "pairing",
637+storeAllowFrom: [],
638+historyLimit: 0,
639+groupHistories: new Map(),
640+});
641+642+expect(decision).toEqual({ kind: "drop", reason: "control command (unauthorized)" });
643+});
644+645+it("does not authorize group control commands from legacy conversation allowFrom entries", async () => {
646+const cfg = baseCfg();
647+cfg.channels ??= {};
648+cfg.channels.imessage ??= {};
649+cfg.channels.imessage.groupPolicy = "allowlist";
650+651+const decision = await resolveIMessageInboundDecision({
652+ cfg,
653+accountId: "default",
654+message: {
655+id: 36,
656+chat_id: 101,
657+sender: "+15550003333",
658+is_from_me: false,
659+text: "/status",
660+is_group: true,
661+},
662+opts: {},
663+messageText: "/status",
664+bodyText: "/status",
665+allowFrom: ["chat_id:101"],
666+groupAllowFrom: [],
667+allowLegacyConversationAllowFromForGroup: true,
668+groupPolicy: "allowlist",
669+dmPolicy: "pairing",
670+storeAllowFrom: [],
671+historyLimit: 0,
672+groupHistories: new Map(),
673+});
674+675+expect(decision).toEqual({ kind: "drop", reason: "control command (unauthorized)" });
676+});
677+435678it("blocks group messages when groupPolicy is disabled", async () => {
436679const cfg = baseCfg();
437680cfg.channels ??= {};
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。