antenna-openclaw-plugin 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/index.ts +93 -25
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -119,6 +119,28 @@ function cronJobId(deviceA: string, deviceB: string): string {
119
119
  return `antenna-follow-${safe(deviceA)}-${safe(deviceB)}`;
120
120
  }
121
121
 
122
+ /** Send a real-time notification to a user via openclaw agent --deliver */
123
+ function notifyUser(
124
+ channel: string,
125
+ userId: string,
126
+ message: string,
127
+ logger: any,
128
+ ): void {
129
+ try {
130
+ execSync(
131
+ `openclaw agent` +
132
+ ` --message ${JSON.stringify(message)}` +
133
+ ` --deliver` +
134
+ ` --reply-channel ${channel}` +
135
+ ` --reply-to "${userId}"`,
136
+ { timeout: 30_000, encoding: "utf-8" },
137
+ );
138
+ logger.info(`Antenna: notified ${channel}:${userId}`);
139
+ } catch (err: any) {
140
+ logger.warn(`Antenna: notify failed for ${channel}:${userId}: ${err.message}`);
141
+ }
142
+ }
143
+
122
144
  function startFollowUpCron(
123
145
  deviceId: string,
124
146
  targetDeviceId: string,
@@ -142,9 +164,6 @@ function startFollowUpCron(
142
164
  `如果还没有 mutual,回复 HEARTBEAT_OK。`,
143
165
  ].join(" ");
144
166
 
145
- // Schedule: expire after 2h using --at for auto-cleanup
146
- const expiresAt = new Date(Date.now() + FOLLOW_UP_MAX_DURATION_MS).toISOString();
147
-
148
167
  try {
149
168
  // Create recurring 15-min job
150
169
  execSync(
@@ -512,16 +531,16 @@ export default function register(api: any) {
512
531
  });
513
532
 
514
533
  // ═══════════════════════════════════════════════════════════════════
515
- // Service: poll for new mutual matches every 10 minutes
534
+ // Service: poll for new matches every 10 minutes → notify instantly
516
535
  // ═══════════════════════════════════════════════════════════════════
517
- const _pendingNotifications: Map<string, any[]> = new Map(); // deviceId new mutual matches
536
+ const _notifiedMatches = new Set<string>(); // "deviceAdeviceB" already notified
518
537
 
519
538
  let _pollTimer: ReturnType<typeof setInterval> | null = null;
520
539
 
521
540
  api.registerService({
522
541
  id: "antenna-match-poller",
523
542
  start: () => {
524
- logger.info("Antenna: match poller started (10 min interval)");
543
+ logger.info("Antenna: match poller started (10 min interval, real-time notify)");
525
544
  _pollTimer = setInterval(async () => {
526
545
  try {
527
546
  const cfg = getConfig(api);
@@ -539,15 +558,74 @@ export default function register(api: any) {
539
558
  const { data: matches } = await supabase.rpc("get_my_matches", { p_device_id: deviceId });
540
559
  if (!matches?.length) continue;
541
560
 
542
- // Check for matches created in last 10 min (new since last poll)
561
+ // Find new matches created in last 10 min
543
562
  const newMatches = matches.filter((m: any) => {
544
563
  const created = new Date(m.created_at).getTime();
545
- return Date.now() - created < 10 * 60 * 1000;
564
+ const key = `${m.device_id_a}→${m.device_id_b}`;
565
+ return Date.now() - created < 10 * 60 * 1000 && !_notifiedMatches.has(key);
546
566
  });
547
567
 
548
- if (newMatches.length > 0) {
549
- _pendingNotifications.set(deviceId, newMatches);
550
- logger.info(`Antenna: ${newMatches.length} new match(es) for ${deviceId}`);
568
+ if (newMatches.length === 0) continue;
569
+
570
+ // Parse channel and userId from device_id (format: "channel:userId")
571
+ const parts = deviceId.split(":");
572
+ if (parts.length < 2) continue;
573
+ const channel = parts[0];
574
+ const userId = parts.slice(1).join(":");
575
+
576
+ // Check for mutual matches
577
+ const myMatches = matches.filter((m: any) => m.device_id_a === deviceId);
578
+ const incomingMatches = matches.filter((m: any) => m.device_id_b === deviceId);
579
+
580
+ for (const match of newMatches) {
581
+ const notifyKey = `${match.device_id_a}→${match.device_id_b}`;
582
+ _notifiedMatches.add(notifyKey);
583
+
584
+ // Is this a new mutual match?
585
+ if (match.device_id_a === deviceId) {
586
+ const reverse = incomingMatches.find((m: any) => m.device_id_a === match.device_id_b);
587
+ if (reverse) {
588
+ const { data: theirProfile } = await supabase.rpc("get_profile", { p_device_id: match.device_id_b });
589
+ const name = theirProfile?.display_name || "对方";
590
+ const emoji = theirProfile?.emoji || "👤";
591
+ const contact = reverse.contact_info_a ? `\n对方的联系方式:${reverse.contact_info_a}` : "";
592
+ notifyUser(
593
+ channel, userId,
594
+ `[Antenna] 🎉 双向匹配成功!${emoji} ${name} 也接受了你!${contact}\n\n用 antenna_check_matches 查看详情。`,
595
+ logger,
596
+ );
597
+ // Clean up follow-up crons
598
+ stopFollowUpCron(deviceId, match.device_id_b, logger);
599
+ }
600
+ } else if (match.device_id_b === deviceId) {
601
+ // Someone new accepted me
602
+ const { data: theirProfile } = await supabase.rpc("get_profile", { p_device_id: match.device_id_a });
603
+ const name = theirProfile?.display_name || "有人";
604
+ const emoji = theirProfile?.emoji || "👤";
605
+ const iAccepted = myMatches.find((m: any) => m.device_id_b === match.device_id_a);
606
+ if (iAccepted) {
607
+ // I already accepted them → mutual!
608
+ const contact = match.contact_info_a ? `\n对方的联系方式:${match.contact_info_a}` : "";
609
+ notifyUser(
610
+ channel, userId,
611
+ `[Antenna] 🎉 双向匹配成功!${emoji} ${name} 也接受了你!${contact}\n\n用 antenna_check_matches 查看详情。`,
612
+ logger,
613
+ );
614
+ stopFollowUpCron(deviceId, match.device_id_a, logger);
615
+ } else {
616
+ // They accepted me but I haven't responded
617
+ notifyUser(
618
+ channel, userId,
619
+ `[Antenna] 📩 ${emoji} ${name} 想认识你!看看 TA 的名片,决定要不要接受?\n\n用 antenna_check_matches 查看详情。`,
620
+ logger,
621
+ );
622
+ }
623
+ }
624
+ }
625
+
626
+ // Prune old entries from _notifiedMatches (keep last 24h)
627
+ if (_notifiedMatches.size > 5000) {
628
+ _notifiedMatches.clear();
551
629
  }
552
630
  }
553
631
  } catch (err: any) {
@@ -562,7 +640,7 @@ export default function register(api: any) {
562
640
  });
563
641
 
564
642
  // ═══════════════════════════════════════════════════════════════════
565
- // Hook: auto-scan when location is received + inject match notifications
643
+ // Hook: auto-scan when location is received
566
644
  // ═══════════════════════════════════════════════════════════════════
567
645
  api.on(
568
646
  "before_prompt_build",
@@ -571,26 +649,16 @@ export default function register(api: any) {
571
649
  const cfg = getConfig(api);
572
650
  let hint = "";
573
651
 
574
- // --- Check for pending match notifications ---
575
- if (ctx?.senderId && ctx?.channel) {
576
- const deviceId = deriveDeviceId(ctx.senderId, ctx.channel);
577
- const pending = _pendingNotifications.get(deviceId);
578
- if (pending && pending.length > 0) {
579
- _pendingNotifications.delete(deviceId);
580
- hint += `\n\n[Antenna] 🎉 有 ${pending.length} 个新的匹配通知!请调用 antenna_check_matches 查看详情,并告诉用户有人想认识他们。`;
581
- }
582
- }
583
-
584
652
  // --- Auto-scan on location ---
585
- if (cfg.autoScanOnLocation === false) return hint ? { prependContext: hint } : {};
653
+ if (cfg.autoScanOnLocation === false) return {};
586
654
 
587
655
  const lat = ctx?.LocationLat;
588
656
  const lon = ctx?.LocationLon;
589
- if (lat == null || lon == null) return hint ? { prependContext: hint } : {};
657
+ if (lat == null || lon == null) return {};
590
658
 
591
659
  const isLive = ctx?.LocationIsLive ?? false;
592
660
  const locationName = ctx?.LocationName ?? "";
593
- hint += isLive
661
+ hint = isLive
594
662
  ? `\n\n[Antenna] 📡 收到实时位置 (${lat.toFixed(4)}, ${lon.toFixed(4)})${locationName ? ` — ${locationName}` : ""}。请使用 antenna_scan 工具查看附近有谁。参数:lat=${lat}, lng=${lon}, sender_id 和 channel 从消息上下文获取。`
595
663
  : `\n\n[Antenna] 📍 收到位置 (${lat.toFixed(4)}, ${lon.toFixed(4)})${locationName ? ` — ${locationName}` : ""}。请使用 antenna_scan 工具查看附近有谁。参数:lat=${lat}, lng=${lon}, sender_id 和 channel 从消息上下文获取。`;
596
664
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antenna-openclaw-plugin",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Antenna — agent-mediated nearby people discovery for OpenClaw",
5
5
  "openclaw": {
6
6
  "extensions": ["./index.ts"]