antenna-fyi 1.2.27 → 1.2.28
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/lib/hermes-plugin/__init__.py +135 -2
- package/package.json +1 -1
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
"""Antenna — Hermes Agent Plugin
|
|
2
2
|
|
|
3
|
-
Nearby people discovery. Registers
|
|
4
|
-
|
|
3
|
+
Nearby people discovery. Registers tools, a pre_llm_call hook,
|
|
4
|
+
and a background Realtime listener for push notifications.
|
|
5
5
|
|
|
6
6
|
Drop this directory into ~/.hermes/plugins/antenna/
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
import threading
|
|
13
|
+
import time
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
9
16
|
from .tools import (
|
|
10
17
|
handle_scan,
|
|
11
18
|
handle_profile,
|
|
@@ -91,6 +98,18 @@ def register(ctx):
|
|
|
91
98
|
|
|
92
99
|
now = time.time()
|
|
93
100
|
|
|
101
|
+
# -1. Read pending notifications from Realtime listener
|
|
102
|
+
_nf = Path.home() / ".antenna" / "pending_notifications.json"
|
|
103
|
+
try:
|
|
104
|
+
if _nf.exists():
|
|
105
|
+
pending = json.loads(_nf.read_text())
|
|
106
|
+
if pending:
|
|
107
|
+
for n in pending:
|
|
108
|
+
hints.append(f"[Antenna] {n['message']}")
|
|
109
|
+
_nf.write_text("[]")
|
|
110
|
+
except Exception:
|
|
111
|
+
pass
|
|
112
|
+
|
|
94
113
|
# 0. Check for new matches (every 60s)
|
|
95
114
|
if now - _last_match_check > _MATCH_CHECK_INTERVAL and _my_device_ids:
|
|
96
115
|
_last_match_check = now
|
|
@@ -185,4 +204,118 @@ def register(ctx):
|
|
|
185
204
|
|
|
186
205
|
ctx.register_hook("pre_llm_call", on_pre_llm)
|
|
187
206
|
|
|
207
|
+
# ── Background Realtime listener for push notifications ──────
|
|
208
|
+
_notif_dir = Path.home() / ".antenna"
|
|
209
|
+
_notif_file = _notif_dir / "pending_notifications.json"
|
|
210
|
+
|
|
211
|
+
def _append_notification(msg: str):
|
|
212
|
+
"""Thread-safe append to pending notifications file."""
|
|
213
|
+
try:
|
|
214
|
+
_notif_dir.mkdir(parents=True, exist_ok=True)
|
|
215
|
+
notifications = []
|
|
216
|
+
if _notif_file.exists():
|
|
217
|
+
try:
|
|
218
|
+
notifications = json.loads(_notif_file.read_text())
|
|
219
|
+
except Exception:
|
|
220
|
+
notifications = []
|
|
221
|
+
notifications.append({"message": msg, "timestamp": time.time()})
|
|
222
|
+
# Keep max 50
|
|
223
|
+
notifications = notifications[-50:]
|
|
224
|
+
_notif_file.write_text(json.dumps(notifications, ensure_ascii=False))
|
|
225
|
+
except Exception:
|
|
226
|
+
pass
|
|
227
|
+
|
|
228
|
+
def _realtime_listener():
|
|
229
|
+
"""Background thread: listen to Supabase Realtime for matches + event participants."""
|
|
230
|
+
try:
|
|
231
|
+
sb = _sb()
|
|
232
|
+
# Listen to matches INSERT
|
|
233
|
+
sb.channel("hermes-antenna-matches").on(
|
|
234
|
+
"postgres_changes",
|
|
235
|
+
{"event": "INSERT", "schema": "public", "table": "matches"},
|
|
236
|
+
lambda payload: _handle_match_change(payload),
|
|
237
|
+
).subscribe()
|
|
238
|
+
|
|
239
|
+
# Listen to event_participants INSERT + UPDATE
|
|
240
|
+
sb.channel("hermes-antenna-events").on(
|
|
241
|
+
"postgres_changes",
|
|
242
|
+
{"event": "*", "schema": "public", "table": "event_participants"},
|
|
243
|
+
lambda payload: _handle_event_change(payload),
|
|
244
|
+
).subscribe()
|
|
245
|
+
|
|
246
|
+
# Keep thread alive
|
|
247
|
+
while True:
|
|
248
|
+
time.sleep(60)
|
|
249
|
+
except Exception as e:
|
|
250
|
+
print(f"[Antenna] Realtime listener failed: {e}")
|
|
251
|
+
|
|
252
|
+
def _handle_match_change(payload):
|
|
253
|
+
try:
|
|
254
|
+
row = payload.get("new") or payload.get("record", {})
|
|
255
|
+
target = row.get("device_id_b")
|
|
256
|
+
if not target or target not in _my_device_ids:
|
|
257
|
+
return
|
|
258
|
+
key = f"{row.get('device_id_a')}→{target}"
|
|
259
|
+
if key in _notified_match_keys:
|
|
260
|
+
return
|
|
261
|
+
_notified_match_keys.add(key)
|
|
262
|
+
sb = _sb()
|
|
263
|
+
prof = sb.rpc("get_profile", {"p_device_id": row.get("device_id_a")}).execute()
|
|
264
|
+
p = prof.data or {}
|
|
265
|
+
name = p.get("display_name") or "有人"
|
|
266
|
+
emoji = p.get("emoji") or "👤"
|
|
267
|
+
_append_notification(f"📩 {emoji} {name} 想认识你!用 antenna_check_matches 查看详情。")
|
|
268
|
+
except Exception:
|
|
269
|
+
pass
|
|
270
|
+
|
|
271
|
+
def _handle_event_change(payload):
|
|
272
|
+
try:
|
|
273
|
+
event_type = payload.get("type") or payload.get("eventType", "")
|
|
274
|
+
row = payload.get("new") or payload.get("record", {})
|
|
275
|
+
old = payload.get("old") or payload.get("old_record", {})
|
|
276
|
+
|
|
277
|
+
if event_type == "INSERT" and row.get("status") == "pending":
|
|
278
|
+
# Someone applied — notify creator
|
|
279
|
+
event_id = row.get("event_id")
|
|
280
|
+
applicant_id = row.get("device_id")
|
|
281
|
+
if not event_id:
|
|
282
|
+
return
|
|
283
|
+
sb = _sb()
|
|
284
|
+
evt = sb.rpc("get_event_by_id", {"p_event_id": event_id}).execute()
|
|
285
|
+
event = evt.data or {}
|
|
286
|
+
if not event.get("found") or event.get("created_by") not in _my_device_ids:
|
|
287
|
+
return
|
|
288
|
+
prof = sb.rpc("get_profile", {"p_device_id": applicant_id}).execute()
|
|
289
|
+
p = prof.data or {}
|
|
290
|
+
name = p.get("display_name") or "某人"
|
|
291
|
+
emoji = p.get("emoji") or "👤"
|
|
292
|
+
_append_notification(f"📩 {emoji} {name} 申请加入你的活动「{event.get('name')}」!用 antenna_event_scan 查看并审批。")
|
|
293
|
+
|
|
294
|
+
elif event_type == "UPDATE":
|
|
295
|
+
device_id = row.get("device_id")
|
|
296
|
+
if device_id not in _my_device_ids:
|
|
297
|
+
return
|
|
298
|
+
old_status = old.get("status")
|
|
299
|
+
new_status = row.get("status")
|
|
300
|
+
if old_status == "pending" and new_status == "active":
|
|
301
|
+
sb = _sb()
|
|
302
|
+
evt = sb.rpc("get_event_by_id", {"p_event_id": row.get("event_id")}).execute()
|
|
303
|
+
event = evt.data or {}
|
|
304
|
+
_append_notification(f"✅ 你的申请已通过!欢迎加入「{event.get('name', '活动')}」")
|
|
305
|
+
elif old_status == "pending" and new_status == "rejected":
|
|
306
|
+
sb = _sb()
|
|
307
|
+
evt = sb.rpc("get_event_by_id", {"p_event_id": row.get("event_id")}).execute()
|
|
308
|
+
event = evt.data or {}
|
|
309
|
+
_append_notification(f"❌ 你的申请未通过「{event.get('name', '活动')}」")
|
|
310
|
+
except Exception:
|
|
311
|
+
pass
|
|
312
|
+
|
|
313
|
+
# Start background listener thread
|
|
314
|
+
try:
|
|
315
|
+
t = threading.Thread(target=_realtime_listener, daemon=True, name="antenna-realtime")
|
|
316
|
+
t.start()
|
|
317
|
+
print("[Antenna] Realtime listener started 📡")
|
|
318
|
+
except Exception as e:
|
|
319
|
+
print(f"[Antenna] Failed to start Realtime listener: {e}")
|
|
320
|
+
|
|
188
321
|
print("[Antenna] Plugin loaded 📡")
|