antenna-openclaw-plugin 1.2.1 → 1.2.2

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 +86 -18
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -47,6 +47,7 @@ let _supabaseClient: SupabaseClient | null = null;
47
47
  let _supabaseUrl: string | null = null;
48
48
  const _lastScanTime = new Map<string, number>();
49
49
  const SCAN_DEBOUNCE_MS = 30_000;
50
+ const _knownDeviceIds = new Set<string>();
50
51
 
51
52
  function getConfig(api: any): AntennaConfig {
52
53
  const cfg = api.config?.plugins?.entries?.antenna?.config ?? {};
@@ -99,7 +100,9 @@ function extractWords(profile: Partial<Profile>): string[] {
99
100
  }
100
101
 
101
102
  function deriveDeviceId(senderId: string, channel: string): string {
102
- return `${channel}:${senderId}`;
103
+ const id = `${channel}:${senderId}`;
104
+ _knownDeviceIds.add(id);
105
+ return id;
103
106
  }
104
107
 
105
108
  /** Wrap result as MCP tool response */
@@ -587,23 +590,19 @@ export default function register(api: any) {
587
590
  const theirLines = [p.line1, p.line2, p.line3].filter(Boolean).join(". ");
588
591
  let match_reason: string | null = null;
589
592
 
590
- // Generate match reason with Gemini Flash
593
+ // Generate match reason via Edge Function (no client-side API key needed)
591
594
  if (myLines && theirLines) {
592
595
  try {
593
- const apiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
594
- if (apiKey) {
595
- const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`, {
596
- method: "POST",
597
- headers: { "Content-Type": "application/json" },
598
- body: JSON.stringify({
599
- contents: [{ parts: [{ text: `You are matching two people. Person A: "${myLines}". Person B: "${theirLines}". Write ONE short sentence (under 20 words) in the SAME LANGUAGE as the profiles explaining why they might click. Be specific, not generic. No fluff.` }] }],
600
- generationConfig: { maxOutputTokens: 60, temperature: 0.7 },
601
- }),
602
- });
603
- if (res.ok) {
604
- const data = await res.json();
605
- match_reason = data?.candidates?.[0]?.content?.parts?.[0]?.text?.trim() || null;
606
- }
596
+ const supabaseUrl = cfg.supabaseUrl || BUILTIN_SUPABASE_URL;
597
+ const supabaseKey = cfg.supabaseKey || BUILTIN_SUPABASE_ANON_KEY;
598
+ const res = await fetch(`${supabaseUrl}/functions/v1/generate-match-reason`, {
599
+ method: "POST",
600
+ headers: { "Content-Type": "application/json", "Authorization": `Bearer ${supabaseKey}` },
601
+ body: JSON.stringify({ my_lines: myLines, their_lines: theirLines }),
602
+ });
603
+ if (res.ok) {
604
+ const data = await res.json();
605
+ match_reason = data?.reason || null;
607
606
  }
608
607
  } catch { /* best effort */ }
609
608
  }
@@ -849,11 +848,72 @@ export default function register(api: any) {
849
848
  const _notifiedMatches = new Set<string>(); // "deviceA→deviceB" already notified
850
849
 
851
850
  let _pollTimer: ReturnType<typeof setInterval> | null = null;
851
+ let _realtimeChannel: any = null;
852
852
 
853
853
  api.registerService({
854
854
  id: "antenna-match-poller",
855
855
  start: () => {
856
- logger.info("Antenna: match poller started (10 min interval, real-time notify)");
856
+ logger.info("Antenna: match poller started (10 min interval + Supabase Realtime)");
857
+
858
+ // ── Supabase Realtime: instant match notifications ──────────
859
+ try {
860
+ const rtCfg = getConfig(api);
861
+ const rtSupabase = getSupabase(rtCfg);
862
+ _realtimeChannel = rtSupabase
863
+ .channel('antenna-match-notify')
864
+ .on('postgres_changes',
865
+ { event: 'INSERT', schema: 'public', table: 'matches' },
866
+ async (payload: any) => {
867
+ try {
868
+ const targetDeviceId = payload.new?.device_id_b;
869
+ if (!targetDeviceId || !_knownDeviceIds.has(targetDeviceId)) return;
870
+
871
+ const key = `${payload.new.device_id_a}→${targetDeviceId}`;
872
+ if (_notifiedMatches.has(key)) return;
873
+ _notifiedMatches.add(key);
874
+
875
+ const parts = targetDeviceId.split(":");
876
+ if (parts.length < 2) return;
877
+ const channel = parts[0];
878
+ const userId = parts.slice(1).join(":");
879
+
880
+ const innerCfg = getConfig(api);
881
+ const innerSb = getSupabase(innerCfg);
882
+
883
+ const { data: theirProfile } = await innerSb.rpc("get_profile", { p_device_id: payload.new.device_id_a });
884
+ const name = theirProfile?.display_name || "有人";
885
+ const emoji = theirProfile?.emoji || "👤";
886
+
887
+ // Check if mutual
888
+ const { data: matches } = await innerSb.rpc("get_my_matches", { p_device_id: targetDeviceId });
889
+ const myAccept = (matches || []).find(
890
+ (m: any) => m.device_id_a === targetDeviceId && m.device_id_b === payload.new.device_id_a
891
+ );
892
+
893
+ if (myAccept) {
894
+ const contact = payload.new.contact_info_a ? `\n对方的联系方式:${payload.new.contact_info_a}` : "";
895
+ notifyUser(channel, userId,
896
+ `[Antenna] 🎉 双向匹配!${emoji} ${name} 也接受了你!${contact}\n\n用 antenna_check_matches 查看详情。`,
897
+ logger);
898
+ stopFollowUpCron(targetDeviceId, payload.new.device_id_a, logger);
899
+ } else {
900
+ notifyUser(channel, userId,
901
+ `[Antenna] 📩 ${emoji} ${name} 想认识你!看看 TA 的名片,决定要不要接受?\n\n用 antenna_check_matches 查看详情。`,
902
+ logger);
903
+ }
904
+ } catch (err: any) {
905
+ logger.warn("Antenna: realtime match handler error:", err.message);
906
+ }
907
+ }
908
+ )
909
+ .subscribe((status: string) => {
910
+ logger.info(`Antenna: realtime subscription status: ${status}`);
911
+ });
912
+ } catch (err: any) {
913
+ logger.warn("Antenna: failed to start realtime subscription, falling back to poll only:", err.message);
914
+ }
915
+
916
+ // ── Poll fallback: catch anything Realtime missed ───────────
857
917
  _pollTimer = setInterval(async () => {
858
918
  try {
859
919
  const cfg = getConfig(api);
@@ -948,7 +1008,15 @@ export default function register(api: any) {
948
1008
  },
949
1009
  stop: () => {
950
1010
  if (_pollTimer) clearInterval(_pollTimer);
951
- logger.info("Antenna: match poller stopped");
1011
+ if (_realtimeChannel) {
1012
+ try {
1013
+ const rtCfg = getConfig(api);
1014
+ const rtSupabase = getSupabase(rtCfg);
1015
+ rtSupabase.removeChannel(_realtimeChannel);
1016
+ } catch { /* best effort */ }
1017
+ _realtimeChannel = null;
1018
+ }
1019
+ logger.info("Antenna: match poller + realtime stopped");
952
1020
  },
953
1021
  });
954
1022
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antenna-openclaw-plugin",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "Antenna — agent-mediated nearby people discovery for OpenClaw",
5
5
  "openclaw": {
6
6
  "extensions": ["./index.ts"]