antenna-openclaw-plugin 0.2.2 → 0.3.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 CHANGED
@@ -331,22 +331,85 @@ export default function register(api: any) {
331
331
  });
332
332
 
333
333
  // ═══════════════════════════════════════════════════════════════════
334
- // Hook: auto-scan when location is received
334
+ // Service: poll for new mutual matches every 10 minutes
335
+ // ═══════════════════════════════════════════════════════════════════
336
+ const _pendingNotifications: Map<string, any[]> = new Map(); // deviceId → new mutual matches
337
+
338
+ let _pollTimer: ReturnType<typeof setInterval> | null = null;
339
+
340
+ api.registerService({
341
+ id: "antenna-match-poller",
342
+ start: () => {
343
+ logger.info("Antenna: match poller started (10 min interval)");
344
+ _pollTimer = setInterval(async () => {
345
+ try {
346
+ const cfg = getConfig(api);
347
+ const supabase = getSupabase(cfg);
348
+
349
+ // Get all profiles that have been active in last 24h
350
+ const { data: activeProfiles } = await supabase
351
+ .rpc("nearby_profiles", { p_lat: 0, p_lng: 0, p_radius_m: 999999999 })
352
+ .select("device_id");
353
+
354
+ if (!activeProfiles?.length) return;
355
+
356
+ for (const profile of activeProfiles) {
357
+ const deviceId = profile.device_id;
358
+ const { data: matches } = await supabase.rpc("get_my_matches", { p_device_id: deviceId });
359
+ if (!matches?.length) continue;
360
+
361
+ // Check for matches created in last 10 min (new since last poll)
362
+ const newMatches = matches.filter((m: any) => {
363
+ const created = new Date(m.created_at).getTime();
364
+ return Date.now() - created < 10 * 60 * 1000;
365
+ });
366
+
367
+ if (newMatches.length > 0) {
368
+ _pendingNotifications.set(deviceId, newMatches);
369
+ logger.info(`Antenna: ${newMatches.length} new match(es) for ${deviceId}`);
370
+ }
371
+ }
372
+ } catch (err: any) {
373
+ logger.warn("Antenna: match poll error:", err.message);
374
+ }
375
+ }, 10 * 60 * 1000); // 10 minutes
376
+ },
377
+ stop: () => {
378
+ if (_pollTimer) clearInterval(_pollTimer);
379
+ logger.info("Antenna: match poller stopped");
380
+ },
381
+ });
382
+
383
+ // ═══════════════════════════════════════════════════════════════════
384
+ // Hook: auto-scan when location is received + inject match notifications
335
385
  // ═══════════════════════════════════════════════════════════════════
336
386
  api.on(
337
387
  "before_prompt_build",
338
388
  (event: any, ctx: any) => {
339
389
  try {
340
390
  const cfg = getConfig(api);
341
- if (cfg.autoScanOnLocation === false) return {};
391
+ let hint = "";
392
+
393
+ // --- Check for pending match notifications ---
394
+ if (ctx?.senderId && ctx?.channel) {
395
+ const deviceId = deriveDeviceId(ctx.senderId, ctx.channel);
396
+ const pending = _pendingNotifications.get(deviceId);
397
+ if (pending && pending.length > 0) {
398
+ _pendingNotifications.delete(deviceId);
399
+ hint += `\n\n[Antenna] 🎉 有 ${pending.length} 个新的匹配通知!请调用 antenna_check_matches 查看详情,并告诉用户有人想认识他们。`;
400
+ }
401
+ }
402
+
403
+ // --- Auto-scan on location ---
404
+ if (cfg.autoScanOnLocation === false) return hint ? { prependContext: hint } : {};
342
405
 
343
406
  const lat = ctx?.LocationLat;
344
407
  const lon = ctx?.LocationLon;
345
- if (lat == null || lon == null) return {};
408
+ if (lat == null || lon == null) return hint ? { prependContext: hint } : {};
346
409
 
347
410
  const isLive = ctx?.LocationIsLive ?? false;
348
411
  const locationName = ctx?.LocationName ?? "";
349
- const hint = isLive
412
+ hint += isLive
350
413
  ? `\n\n[Antenna] 📡 收到实时位置 (${lat.toFixed(4)}, ${lon.toFixed(4)})${locationName ? ` — ${locationName}` : ""}。请使用 antenna_scan 工具查看附近有谁。参数:lat=${lat}, lng=${lon}, sender_id 和 channel 从消息上下文获取。`
351
414
  : `\n\n[Antenna] 📍 收到位置 (${lat.toFixed(4)}, ${lon.toFixed(4)})${locationName ? ` — ${locationName}` : ""}。请使用 antenna_scan 工具查看附近有谁。参数:lat=${lat}, lng=${lon}, sender_id 和 channel 从消息上下文获取。`;
352
415
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antenna-openclaw-plugin",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "Antenna — agent-mediated nearby people discovery for OpenClaw",
5
5
  "openclaw": {
6
6
  "extensions": ["./index.ts"]
@@ -8,8 +8,25 @@ metadata: { "openclaw": { "always": true } }
8
8
 
9
9
  You have access to the Antenna plugin tools for location-based social discovery.
10
10
 
11
+ ## Onboarding — 装上之后主动引导
12
+
13
+ Plugin 安装后,agent 应该**主动**开始引导,不要等用户问"怎么用"。
14
+
15
+ ### 流程
16
+
17
+ **第一步:打招呼 + 解释**
18
+ > "嘿,你装了 Antenna——它能帮你发现附近有意思的人。我先帮你做张名片,然后你发个位置给我,我看看附近有谁。"
19
+
20
+ **第二步:聊天式填名片**(见下方详细流程)
21
+
22
+ **第三步:名片存好后,要位置**
23
+ > "名片存好了 ✅ 现在发个位置给我——在 Telegram/WhatsApp 里点位置分享就行。我帮你看看附近有谁值得认识。"
24
+
25
+ **关键:名片先于位置。** 没有名片就扫描,附近的人看不到你的信息,匹配质量也差。
26
+
11
27
  ## When to use
12
28
 
29
+ - **首次安装后**: 主动开始 onboarding(名片 → 位置)
13
30
  - User shares a location (Telegram live location, WhatsApp pin, or tells you where they are)
14
31
  - User asks "附近有谁" / "who's nearby" / "周围有什么人"
15
32
  - User wants to set up or edit their profile card (名片)
@@ -153,10 +170,11 @@ Everything is ephemeral:
153
170
 
154
171
  ### Heartbeat — 自动查匹配
155
172
 
156
- 如果 agent heartbeat 机制(定期唤醒),每次 heartbeat 时:
157
- 1. 检查用户是否有 Antenna profile(`antenna_profile` action="get")
158
- 2. 如果有,调 `antenna_check_matches` 查看是否有新的 mutual match
159
- 3. 如果有新匹配,主动通知用户:"有人想认识你!"并展示对方信息
160
- 4. 如果没有新匹配,不用说话(静默跳过)
173
+ Plugin 自带后台服务,每 10 分钟轮询一次 Supabase 查新的 mutual match。如果发现新匹配,会在用户下次跟 agent 说话时通过 `[Antenna] 🎉` 提示注入 context。
174
+
175
+ 当你看到 `[Antenna] 🎉 X 个新的匹配通知` 时:
176
+ 1. 调 `antenna_check_matches` 拿详情
177
+ 2. 告诉用户:"有人想认识你!" + 展示对方名片
178
+ 3. 如果对方分享了联系方式,一并展示
161
179
 
162
- 这样用户不需要主动问,agent 会自动帮忙盯着。
180
+ 用户不需要主动问,agent 会自动收到通知。