antenna-openclaw-plugin 0.2.3 → 0.3.1

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
@@ -302,51 +302,143 @@ export default function register(api: any) {
302
302
  const deviceId = deriveDeviceId(params.sender_id, params.channel);
303
303
 
304
304
  const { data: allMatches } = await supabase.rpc("get_my_matches", { p_device_id: deviceId });
305
- const myMatches = (allMatches || []).filter((m: any) => m.device_id_a === deviceId);
306
305
 
307
- if (myMatches.length === 0) {
308
- return ok({ mutual_matches: [], message: "目前没有进行中的匹配。" });
306
+ if (!allMatches?.length) {
307
+ return ok({ mutual_matches: [], incoming_accepts: [], message: "目前没有进行中的匹配。" });
309
308
  }
310
309
 
310
+ // Matches I initiated
311
+ const myMatches = allMatches.filter((m: any) => m.device_id_a === deviceId);
312
+ // Matches where someone else accepted me
313
+ const incomingMatches = allMatches.filter((m: any) => m.device_id_b === deviceId);
314
+
315
+ // --- Mutual matches (both sides accepted) ---
311
316
  const mutualMatches = [];
312
317
  for (const match of myMatches) {
313
- const reverse = (allMatches || []).find(
314
- (m: any) => m.device_id_a === match.device_id_b && m.device_id_b === deviceId
318
+ const reverse = incomingMatches.find(
319
+ (m: any) => m.device_id_a === match.device_id_b
315
320
  );
316
321
  if (reverse) {
317
322
  const { data: profile } = await supabase.rpc("get_profile", { p_device_id: match.device_id_b });
318
323
  mutualMatches.push({
324
+ device_id: match.device_id_b,
319
325
  name: profile?.display_name || "匿名", emoji: profile?.emoji || "👤",
326
+ line1: profile?.line1, line2: profile?.line2, line3: profile?.line3,
320
327
  their_contact: reverse.contact_info_a || null, you_shared: match.contact_info_a || null,
321
328
  });
322
329
  }
323
330
  }
324
331
 
325
- if (mutualMatches.length === 0) {
326
- return ok({ mutual_matches: [], message: "你接受了一些匹配,但对方还没有回应。耐心等等 ⏳" });
332
+ // --- Incoming accepts (someone accepted me but I haven't accepted them yet) ---
333
+ const incomingAccepts = [];
334
+ for (const match of incomingMatches) {
335
+ const iAccepted = myMatches.find(
336
+ (m: any) => m.device_id_b === match.device_id_a
337
+ );
338
+ if (!iAccepted) {
339
+ // They accepted me but I haven't responded
340
+ const { data: profile } = await supabase.rpc("get_profile", { p_device_id: match.device_id_a });
341
+ incomingAccepts.push({
342
+ device_id: match.device_id_a,
343
+ name: profile?.display_name || "匿名", emoji: profile?.emoji || "👤",
344
+ line1: profile?.line1, line2: profile?.line2, line3: profile?.line3,
345
+ });
346
+ }
327
347
  }
328
348
 
329
- return ok({ mutual_matches: mutualMatches });
349
+ const messages = [];
350
+ if (mutualMatches.length > 0) messages.push(`${mutualMatches.length} 个双向匹配!可以交换联系方式了`);
351
+ if (incomingAccepts.length > 0) messages.push(`${incomingAccepts.length} 个人想认识你,等你回应`);
352
+ if (messages.length === 0) messages.push("你接受了一些匹配,但对方还没有回应。耐心等等 ⏳");
353
+
354
+ return ok({
355
+ mutual_matches: mutualMatches,
356
+ incoming_accepts: incomingAccepts,
357
+ message: messages.join(";"),
358
+ });
330
359
  },
331
360
  });
332
361
 
333
362
  // ═══════════════════════════════════════════════════════════════════
334
- // Hook: auto-scan when location is received
363
+ // Service: poll for new mutual matches every 10 minutes
364
+ // ═══════════════════════════════════════════════════════════════════
365
+ const _pendingNotifications: Map<string, any[]> = new Map(); // deviceId → new mutual matches
366
+
367
+ let _pollTimer: ReturnType<typeof setInterval> | null = null;
368
+
369
+ api.registerService({
370
+ id: "antenna-match-poller",
371
+ start: () => {
372
+ logger.info("Antenna: match poller started (10 min interval)");
373
+ _pollTimer = setInterval(async () => {
374
+ try {
375
+ const cfg = getConfig(api);
376
+ const supabase = getSupabase(cfg);
377
+
378
+ // Get all profiles that have been active in last 24h
379
+ const { data: activeProfiles } = await supabase
380
+ .rpc("nearby_profiles", { p_lat: 0, p_lng: 0, p_radius_m: 999999999 })
381
+ .select("device_id");
382
+
383
+ if (!activeProfiles?.length) return;
384
+
385
+ for (const profile of activeProfiles) {
386
+ const deviceId = profile.device_id;
387
+ const { data: matches } = await supabase.rpc("get_my_matches", { p_device_id: deviceId });
388
+ if (!matches?.length) continue;
389
+
390
+ // Check for matches created in last 10 min (new since last poll)
391
+ const newMatches = matches.filter((m: any) => {
392
+ const created = new Date(m.created_at).getTime();
393
+ return Date.now() - created < 10 * 60 * 1000;
394
+ });
395
+
396
+ if (newMatches.length > 0) {
397
+ _pendingNotifications.set(deviceId, newMatches);
398
+ logger.info(`Antenna: ${newMatches.length} new match(es) for ${deviceId}`);
399
+ }
400
+ }
401
+ } catch (err: any) {
402
+ logger.warn("Antenna: match poll error:", err.message);
403
+ }
404
+ }, 10 * 60 * 1000); // 10 minutes
405
+ },
406
+ stop: () => {
407
+ if (_pollTimer) clearInterval(_pollTimer);
408
+ logger.info("Antenna: match poller stopped");
409
+ },
410
+ });
411
+
412
+ // ═══════════════════════════════════════════════════════════════════
413
+ // Hook: auto-scan when location is received + inject match notifications
335
414
  // ═══════════════════════════════════════════════════════════════════
336
415
  api.on(
337
416
  "before_prompt_build",
338
417
  (event: any, ctx: any) => {
339
418
  try {
340
419
  const cfg = getConfig(api);
341
- if (cfg.autoScanOnLocation === false) return {};
420
+ let hint = "";
421
+
422
+ // --- Check for pending match notifications ---
423
+ if (ctx?.senderId && ctx?.channel) {
424
+ const deviceId = deriveDeviceId(ctx.senderId, ctx.channel);
425
+ const pending = _pendingNotifications.get(deviceId);
426
+ if (pending && pending.length > 0) {
427
+ _pendingNotifications.delete(deviceId);
428
+ hint += `\n\n[Antenna] 🎉 有 ${pending.length} 个新的匹配通知!请调用 antenna_check_matches 查看详情,并告诉用户有人想认识他们。`;
429
+ }
430
+ }
431
+
432
+ // --- Auto-scan on location ---
433
+ if (cfg.autoScanOnLocation === false) return hint ? { prependContext: hint } : {};
342
434
 
343
435
  const lat = ctx?.LocationLat;
344
436
  const lon = ctx?.LocationLon;
345
- if (lat == null || lon == null) return {};
437
+ if (lat == null || lon == null) return hint ? { prependContext: hint } : {};
346
438
 
347
439
  const isLive = ctx?.LocationIsLive ?? false;
348
440
  const locationName = ctx?.LocationName ?? "";
349
- const hint = isLive
441
+ hint += isLive
350
442
  ? `\n\n[Antenna] 📡 收到实时位置 (${lat.toFixed(4)}, ${lon.toFixed(4)})${locationName ? ` — ${locationName}` : ""}。请使用 antenna_scan 工具查看附近有谁。参数:lat=${lat}, lng=${lon}, sender_id 和 channel 从消息上下文获取。`
351
443
  : `\n\n[Antenna] 📍 收到位置 (${lat.toFixed(4)}, ${lon.toFixed(4)})${locationName ? ` — ${locationName}` : ""}。请使用 antenna_scan 工具查看附近有谁。参数:lat=${lat}, lng=${lon}, sender_id 和 channel 从消息上下文获取。`;
352
444
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antenna-openclaw-plugin",
3
- "version": "0.2.3",
3
+ "version": "0.3.1",
4
4
  "description": "Antenna — agent-mediated nearby people discovery for OpenClaw",
5
5
  "openclaw": {
6
6
  "extensions": ["./index.ts"]
@@ -170,10 +170,11 @@ Everything is ephemeral:
170
170
 
171
171
  ### Heartbeat — 自动查匹配
172
172
 
173
- 如果 agent heartbeat 机制(定期唤醒),每次 heartbeat 时:
174
- 1. 检查用户是否有 Antenna profile(`antenna_profile` action="get")
175
- 2. 如果有,调 `antenna_check_matches` 查看是否有新的 mutual match
176
- 3. 如果有新匹配,主动通知用户:"有人想认识你!"并展示对方信息
177
- 4. 如果没有新匹配,不用说话(静默跳过)
173
+ Plugin 自带后台服务,每 10 分钟轮询一次 Supabase 查新的 mutual match。如果发现新匹配,会在用户下次跟 agent 说话时通过 `[Antenna] 🎉` 提示注入 context。
178
174
 
179
- 这样用户不需要主动问,agent 会自动帮忙盯着。
175
+ 当你看到 `[Antenna] 🎉 有 X 个新的匹配通知` 时:
176
+ 1. 调 `antenna_check_matches` 拿详情
177
+ 2. 告诉用户:"有人想认识你!" + 展示对方名片
178
+ 3. 如果对方分享了联系方式,一并展示
179
+
180
+ 用户不需要主动问,agent 会自动收到通知。