antenna-openclaw-plugin 1.2.0 โ 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 +90 -22
- package/package.json +1 -1
- package/skills/antenna/SKILL.md +7 -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 */
|
|
@@ -288,7 +291,7 @@ export default function register(api: any) {
|
|
|
288
291
|
return { ref, emoji: p.emoji || "๐ค", name: p.display_name || "ๅฟๅ", line1: p.line1, line2: p.line2, line3: p.line3 };
|
|
289
292
|
});
|
|
290
293
|
(api as any)._antennaRefMap = gRefMap;
|
|
291
|
-
try { await supabase.rpc("save_scan_refs", { p_owner: deviceId, p_refs:
|
|
294
|
+
try { await supabase.rpc("save_scan_refs", { p_owner: deviceId, p_refs: gRefMap }); } catch {}
|
|
292
295
|
for (const p of globalOthers) {
|
|
293
296
|
try { await supabase.rpc("log_recommendation", { p_device_id: deviceId, p_recommended_id: p.device_id }); } catch {}
|
|
294
297
|
}
|
|
@@ -318,7 +321,7 @@ export default function register(api: any) {
|
|
|
318
321
|
// Store ref map for accept โ memory + DB
|
|
319
322
|
(api as any)._antennaRefMap = _refMap;
|
|
320
323
|
try {
|
|
321
|
-
await supabase.rpc("save_scan_refs", { p_owner: deviceId, p_refs:
|
|
324
|
+
await supabase.rpc("save_scan_refs", { p_owner: deviceId, p_refs: _refMap });
|
|
322
325
|
} catch { /* best effort */ }
|
|
323
326
|
|
|
324
327
|
return ok({
|
|
@@ -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
|
}
|
|
@@ -614,7 +613,7 @@ export default function register(api: any) {
|
|
|
614
613
|
// Persist refs + log recommendation
|
|
615
614
|
(api as any)._antennaRefMap = { ...(api as any)._antennaRefMap, ..._refMap };
|
|
616
615
|
try {
|
|
617
|
-
await supabase.rpc("save_scan_refs", { p_owner: deviceId, p_refs:
|
|
616
|
+
await supabase.rpc("save_scan_refs", { p_owner: deviceId, p_refs: _refMap });
|
|
618
617
|
} catch { /* best effort */ }
|
|
619
618
|
for (const p of results) {
|
|
620
619
|
await supabase.rpc("log_recommendation", { p_device_id: deviceId, p_recommended_id: p.device_id });
|
|
@@ -720,7 +719,7 @@ export default function register(api: any) {
|
|
|
720
719
|
});
|
|
721
720
|
|
|
722
721
|
(api as any)._antennaRefMap = { ...(api as any)._antennaRefMap, ..._refMap };
|
|
723
|
-
try { await supabase.rpc("save_scan_refs", { p_owner: deviceId, p_refs:
|
|
722
|
+
try { await supabase.rpc("save_scan_refs", { p_owner: deviceId, p_refs: _refMap }); } catch {}
|
|
724
723
|
|
|
725
724
|
return ok({ count: profiles.length, profiles, event: true });
|
|
726
725
|
},
|
|
@@ -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
|
|
package/package.json
CHANGED
package/skills/antenna/SKILL.md
CHANGED
|
@@ -83,7 +83,7 @@ After receiving the nearby profiles, **you decide** who to recommend:
|
|
|
83
83
|
View or update the user's name card.
|
|
84
84
|
- `action`: "get" or "set"
|
|
85
85
|
- `sender_id`, `channel`: from context
|
|
86
|
-
- For "set": `display_name`, `emoji`, `line1`, `line2`, `line3`, `visible`
|
|
86
|
+
- For "set": `display_name`, `emoji`, `line1`, `line2`, `line3`, `visible`, `matching_context`
|
|
87
87
|
|
|
88
88
|
The name card has:
|
|
89
89
|
- **emoji**: a single emoji that represents them
|
|
@@ -91,6 +91,12 @@ The name card has:
|
|
|
91
91
|
- **line1**: who they are / what they do
|
|
92
92
|
- **line2**: what they're into
|
|
93
93
|
- **line3**: what they're looking for right now
|
|
94
|
+
- **matching_context** (optional, not shown to others): A richer description generated by the agent based on what it knows about the user โ career background, tech stack, interests, projects, personality traits. ~200 words. Only used for embedding-based matching, never displayed to other users.
|
|
95
|
+
|
|
96
|
+
**After saving the profile, generate `matching_context` automatically** based on your knowledge of the user (memory, conversations, context). Don't ask the user to write it โ you write it. Example:
|
|
97
|
+
> "Product designer at a tech company in Beijing, focusing on AI search experience. Interested in music (Sakamoto), swimming, cooking, language learning. Recently exploring AI agent ecosystems and social discovery. Looking to connect with AI builders, indie hackers, and creative technologists."
|
|
98
|
+
- **line2**: what they're into
|
|
99
|
+
- **line3**: what they're looking for right now
|
|
94
100
|
|
|
95
101
|
### `antenna_accept`
|
|
96
102
|
Accept a match after the user sees results. Can optionally include contact info to share.
|