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.
- package/index.ts +86 -18
- 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
|
-
|
|
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
|
|
593
|
+
// Generate match reason via Edge Function (no client-side API key needed)
|
|
591
594
|
if (myLines && theirLines) {
|
|
592
595
|
try {
|
|
593
|
-
const
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|