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 +104 -12
- package/package.json +1 -1
- package/skills/antenna/SKILL.md +7 -6
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 (
|
|
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 =
|
|
314
|
-
(m: any) => m.device_id_a === match.device_id_b
|
|
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
|
-
|
|
326
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
package/skills/antenna/SKILL.md
CHANGED
|
@@ -170,10 +170,11 @@ Everything is ephemeral:
|
|
|
170
170
|
|
|
171
171
|
### Heartbeat — 自动查匹配
|
|
172
172
|
|
|
173
|
-
|
|
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
|
-
|
|
175
|
+
当你看到 `[Antenna] 🎉 有 X 个新的匹配通知` 时:
|
|
176
|
+
1. 调 `antenna_check_matches` 拿详情
|
|
177
|
+
2. 告诉用户:"有人想认识你!" + 展示对方名片
|
|
178
|
+
3. 如果对方分享了联系方式,一并展示
|
|
179
|
+
|
|
180
|
+
用户不需要主动问,agent 会自动收到通知。
|