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.
- package/index.ts +93 -25
- 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
|
|
534
|
+
// Service: poll for new matches every 10 minutes → notify instantly
|
|
516
535
|
// ═══════════════════════════════════════════════════════════════════
|
|
517
|
-
const
|
|
536
|
+
const _notifiedMatches = new Set<string>(); // "deviceA→deviceB" 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
|
-
//
|
|
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
|
-
|
|
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
|
|
549
|
-
|
|
550
|
-
|
|
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
|
|
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
|
|
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
|
|
657
|
+
if (lat == null || lon == null) return {};
|
|
590
658
|
|
|
591
659
|
const isLive = ctx?.LocationIsLive ?? false;
|
|
592
660
|
const locationName = ctx?.LocationName ?? "";
|
|
593
|
-
hint
|
|
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
|
|