/** * Mention Protocol — Bot-to-Bot communication in mention-only groups * * Pure function module — no side effects, no shared state. * * When a group has gm_req_mention=1, bots need to: * 1. Know the group's "social rule" (must @-mention to trigger others) * 2. Know who's in the group (member list) * 3. Have their text-level @mentions translated to TIM protocol-level atUserList * * See: docs/audit/012-mention-mode-bot-to-bot-v3.4.4.md */ // ── Types ── export interface MentionMember { id: string; nick: string; } // ── Regex utility ── function escapeRegex(s: string): string { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } // ── Prompt injection ── /** * Build the mention protocol prompt to inject into GroupSystemPrompt. * Tells the AI: this group is mention-only, here are the members, * you must @mention them to communicate. * * Returns empty string if no other members exist. */ export function buildMentionPrompt( members: MentionMember[], selfId: string, ): string { const others = members.filter(m => m.id !== selfId); if (others.length === 0) return ''; const memberList = others .map(m => `- ${m.nick} (mention as: @${m.nick})`) .join('\n'); return [ '[GROUP COMMUNICATION PROTOCOL]', 'This group uses mention-only mode.', 'Your messages will ONLY be seen by members you @-mention.', 'To mention someone, write @their_name in your message.', '', 'Group members:', memberList, '', `Example: "@${others[0].nick} what do you think?"`, '[/GROUP COMMUNICATION PROTOCOL]', ].join('\n'); } // ── Text → atUserList extraction ── /** * Extract @-mention targets from AI output text. * * Strategy: iterate the known member list and search for each nick * in the text. This is more robust than parsing @-tokens from text, * because it handles: * - Space after @: "@ 投资人Bot" * - Case mismatch: "@投资人bot" * - Trailing punct: "@投资人Bot,你好" * - Multiple @: "@BotA @BotB hi" * * Only matches when @ prefix is present — mentioning a name without @ * (e.g. "投资人Bot说得对") does NOT trigger a match. * * Returns an array of userIDs suitable for TIM createTextAtMessage. * Returns empty array if no matches found. */ export function extractAtTargets( text: string, members: MentionMember[], selfId: string, ): string[] { const results: string[] = []; for (const member of members) { if (member.id === selfId) continue; const pattern = new RegExp(`@\\s*${escapeRegex(member.nick)}`, 'i'); if (pattern.test(text)) { results.push(member.id); } } return results; }