antenna-fyi 1.2.1 → 1.2.3

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/bin/antenna.js CHANGED
@@ -7,7 +7,10 @@ import {
7
7
  handleAccept,
8
8
  handleCheckin,
9
9
  handleMatches,
10
+ handleDiscover,
11
+ handleEvent,
10
12
  handleBind,
13
+ handlePass,
11
14
  handleSetup,
12
15
  handleStatus,
13
16
  handleInstallSkill,
@@ -31,8 +34,14 @@ async function main() {
31
34
  return handleCheckin(f);
32
35
  case "matches":
33
36
  return handleMatches(f);
37
+ case "discover":
38
+ return handleDiscover(f);
39
+ case "event":
40
+ return handleEvent(f);
34
41
  case "bind":
35
42
  return handleBind(f);
43
+ case "pass":
44
+ return handlePass(f);
36
45
  case "serve": {
37
46
  const { startMcpServer } = await import("../lib/mcp.js");
38
47
  return startMcpServer();
package/lib/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // antenna CLI command handlers
2
2
 
3
- import { scan, getProfile, setProfile, accept, checkMatches, checkin, createBindToken } from "./core.js";
3
+ import { scan, getProfile, setProfile, accept, checkMatches, checkin, createBindToken, discover, createEvent, joinEvent, eventScan, pass as passUser } from "./core.js";
4
4
  import { createInterface } from "readline";
5
5
  import { existsSync, mkdirSync, copyFileSync } from "fs";
6
6
  import { join, dirname } from "path";
@@ -24,10 +24,10 @@ export function parseFlags(args) {
24
24
  }
25
25
 
26
26
  export async function handleScan(f) {
27
- if (!f.lat || !f.lng) return console.error("Usage: antenna scan --lat 39.99 --lng 116.48 [--radius 500] (max 1000) [--id telegram:123]");
27
+ if (!f.lat && !f.lng && !f.id) return console.error("Usage: antenna scan --lat 39.99 --lng 116.48 [--radius 500] (max 1000) [--id telegram:123]\n Or just: antenna scan --id telegram:123 (uses saved location from GPS bind)");
28
28
  const result = await scan({
29
- lat: +f.lat,
30
- lng: +f.lng,
29
+ lat: f.lat ? +f.lat : undefined,
30
+ lng: f.lng ? +f.lng : undefined,
31
31
  radius_m: +(f.radius || 500),
32
32
  device_id: f.id || null,
33
33
  });
@@ -110,6 +110,65 @@ export async function handleMatches(f) {
110
110
  }
111
111
  }
112
112
 
113
+ export async function handleDiscover(f) {
114
+ if (!f.id) return console.error("Usage: antenna discover --id telegram:123");
115
+ const result = await discover({ device_id: f.id });
116
+ if (result.count === 0) return console.log(result.message || "🌍 No global recommendation available right now.");
117
+ console.log(`🌍 Global discover:\n`);
118
+ result.profiles.forEach((p) => {
119
+ console.log(` ${p.emoji} ${p.name}`);
120
+ if (p.line1) console.log(` ${p.line1}`);
121
+ if (p.line2) console.log(` ${p.line2}`);
122
+ if (p.line3) console.log(` ${p.line3}`);
123
+ if (p.match_reason) console.log(` → ${p.match_reason}`);
124
+ console.log(` ref: ${p.ref}\n`);
125
+ });
126
+ }
127
+
128
+ export async function handleEvent(f) {
129
+ const sub = f._?.[0] || Object.keys(f).find(k => ["create", "join", "scan"].includes(k));
130
+
131
+ if (f.create || (!f.join && !f.scan && f.name)) {
132
+ if (!f.name) return console.error("Usage: antenna event --create --name 'AI Meetup'");
133
+ const result = await createEvent({ name: f.name, device_id: f.id || null, lat: f.lat ? +f.lat : undefined, lng: f.lng ? +f.lng : undefined });
134
+ console.log(`\n🎉 Event created!\n`);
135
+ console.log(` Name: ${result.name}`);
136
+ console.log(` Code: ${result.code}`);
137
+ console.log(` URL: ${result.url}`);
138
+ console.log(` Ends: ${result.ends_at}\n`);
139
+ return;
140
+ }
141
+
142
+ if (f.join) {
143
+ if (!f.code || !f.id) return console.error("Usage: antenna event --join --code abc123 --id telegram:123");
144
+ const result = await joinEvent({ code: f.code, device_id: f.id });
145
+ if (result.joined) {
146
+ console.log(`\n✅ Joined "${result.name}" (${result.code})\n`);
147
+ } else {
148
+ console.log(`\n❌ ${result.error}\n`);
149
+ }
150
+ return;
151
+ }
152
+
153
+ if (f.scan) {
154
+ if (!f.code) return console.error("Usage: antenna event --scan --code abc123 [--id telegram:123]");
155
+ const result = await eventScan({ code: f.code, device_id: f.id || null });
156
+ if (result.count === 0) return console.log("\n🏟️ No participants yet.\n");
157
+ console.log(`\n🏟️ ${result.count} people in this event:\n`);
158
+ result.profiles.forEach((p) => {
159
+ console.log(` ${p.emoji} ${p.name}`);
160
+ if (p.line1) console.log(` ${p.line1}`);
161
+ console.log(` ref: ${p.ref}\n`);
162
+ });
163
+ return;
164
+ }
165
+
166
+ console.log(`Usage:
167
+ antenna event --create --name 'AI Meetup' [--id telegram:123]
168
+ antenna event --join --code abc123 --id telegram:123
169
+ antenna event --scan --code abc123 [--id telegram:123]`);
170
+ }
171
+
113
172
  export async function handleBind(f) {
114
173
  if (!f.id) return console.error("Usage: antenna bind --id telegram:123");
115
174
  const result = await createBindToken({ device_id: f.id });
@@ -119,6 +178,17 @@ export async function handleBind(f) {
119
178
  console.log();
120
179
  }
121
180
 
181
+ export async function handlePass(f) {
182
+ if (!f.id) return console.error("Usage: antenna pass --id telegram:123 --target telegram:789");
183
+ if (!f.target && !f.ref) return console.error("Usage: antenna pass --id telegram:123 --target telegram:789 (or --ref 1)");
184
+ const result = await passUser({
185
+ device_id: f.id,
186
+ target_device_id: f.target,
187
+ ref: f.ref,
188
+ });
189
+ console.log("✅ " + (result.message || "Passed."));
190
+ }
191
+
122
192
  export async function handleSetup(f) {
123
193
  const rl = createInterface({ input: process.stdin, output: process.stdout });
124
194
  const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
@@ -288,7 +358,10 @@ Usage:
288
358
  antenna checkin --id telegram:123 --lat 39.99 --lng 116.48
289
359
  antenna profile --id telegram:123 [--name Yi --emoji 🦦 --line1 '...']
290
360
  antenna accept --id telegram:123 --target telegram:789 [--contact 'WeChat: yi']
361
+ antenna pass --id telegram:123 --target telegram:789 (or --ref 1)
291
362
  antenna matches --id telegram:123
363
+ antenna discover --id telegram:123
364
+ antenna event --create --name 'AI Meetup' | --join --code abc123 | --scan --code abc123
292
365
  antenna bind --id telegram:123
293
366
  antenna serve Start MCP server (stdio transport)
294
367
  antenna setup Interactive profile setup [--id telegram:123]
package/lib/core.js CHANGED
@@ -10,49 +10,39 @@ const DEFAULT_KEY =
10
10
  let _client = null;
11
11
  let _url = null;
12
12
 
13
- // ─── Embedding ───────────────────────────────────────────────────────
14
-
15
- const GEMINI_EMBEDDING_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-embedding-001:embedContent";
16
- const GEMINI_FLASH_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent";
17
-
18
- async function generateMatchReason(myLines, theirLines) {
19
- const apiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
20
- if (!apiKey) return null;
21
-
22
- const prompt = `You are matching two people. Person A: "${myLines}". Person B: "${theirLines}". Write ONE short sentence (under 20 words) in the SAME LANGUAGE as the profiles explaining why they might click. Be specific, not generic. No fluff.`;
13
+ // ─── Embedding & Match Reason (via Supabase Edge Functions) ───────
23
14
 
15
+ async function generateEmbedding(text) {
24
16
  try {
25
- const res = await fetch(`${GEMINI_FLASH_URL}?key=${apiKey}`, {
17
+ const sb = getClient();
18
+ const res = await fetch(`${_url || DEFAULT_URL}/functions/v1/generate-embedding`, {
26
19
  method: "POST",
27
- headers: { "Content-Type": "application/json" },
28
- body: JSON.stringify({
29
- contents: [{ parts: [{ text: prompt }] }],
30
- generationConfig: { maxOutputTokens: 60, temperature: 0.7 },
31
- }),
20
+ headers: {
21
+ "Content-Type": "application/json",
22
+ "Authorization": `Bearer ${process.env.ANTENNA_SUPABASE_KEY || process.env.ANTENNA_KEY || DEFAULT_KEY}`,
23
+ },
24
+ body: JSON.stringify({ text }),
32
25
  });
33
26
  if (!res.ok) return null;
34
27
  const data = await res.json();
35
- return data?.candidates?.[0]?.content?.parts?.[0]?.text?.trim() || null;
28
+ return data?.embedding || null;
36
29
  } catch { return null; }
37
30
  }
38
31
 
39
- async function generateEmbedding(text) {
40
- const apiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
41
- if (!apiKey) return null; // silently skip if no key
42
-
43
- const res = await fetch(`${GEMINI_EMBEDDING_URL}?key=${apiKey}`, {
44
- method: "POST",
45
- headers: { "Content-Type": "application/json" },
46
- body: JSON.stringify({
47
- content: { parts: [{ text }] },
48
- taskType: "SEMANTIC_SIMILARITY",
49
- outputDimensionality: 768,
50
- }),
51
- });
52
-
53
- if (!res.ok) return null;
54
- const data = await res.json();
55
- return data?.embedding?.values || null;
32
+ async function generateMatchReason(myLines, theirLines) {
33
+ try {
34
+ const res = await fetch(`${_url || DEFAULT_URL}/functions/v1/generate-match-reason`, {
35
+ method: "POST",
36
+ headers: {
37
+ "Content-Type": "application/json",
38
+ "Authorization": `Bearer ${process.env.ANTENNA_SUPABASE_KEY || process.env.ANTENNA_KEY || DEFAULT_KEY}`,
39
+ },
40
+ body: JSON.stringify({ my_lines: myLines, their_lines: theirLines }),
41
+ });
42
+ if (!res.ok) return null;
43
+ const data = await res.json();
44
+ return data?.reason || null;
45
+ } catch { return null; }
56
46
  }
57
47
 
58
48
  export function getClient(url, key) {
@@ -13,14 +13,27 @@ from .tools import (
13
13
  handle_checkin,
14
14
  handle_check_matches,
15
15
  handle_bind,
16
+ handle_pass,
17
+ handle_discover,
18
+ handle_event_create,
19
+ handle_event_join,
20
+ handle_event_scan,
16
21
  _sb,
17
22
  _device_id,
23
+ _my_device_ids,
24
+ )
25
+ from .schemas import (
18
26
  SCAN_SCHEMA,
19
27
  PROFILE_SCHEMA,
20
28
  ACCEPT_SCHEMA,
21
29
  CHECKIN_SCHEMA,
22
30
  CHECK_MATCHES_SCHEMA,
23
31
  BIND_SCHEMA,
32
+ PASS_SCHEMA,
33
+ DISCOVER_SCHEMA,
34
+ EVENT_CREATE_SCHEMA,
35
+ EVENT_JOIN_SCHEMA,
36
+ EVENT_SCAN_SCHEMA,
24
37
  )
25
38
  import re
26
39
  import time
@@ -29,6 +42,11 @@ import time
29
42
  _last_event_check = 0
30
43
  _EVENT_CHECK_INTERVAL = 30 # seconds
31
44
 
45
+ # Track last match check timestamp
46
+ _last_match_check = 0
47
+ _MATCH_CHECK_INTERVAL = 60 # seconds
48
+ _notified_match_keys: set = set() # "deviceA→deviceB" already notified
49
+
32
50
 
33
51
  def register(ctx):
34
52
  # ── Tools ─────────────────────────────────────────────────────
@@ -38,13 +56,65 @@ def register(ctx):
38
56
  ctx.register_tool("antenna_checkin", CHECKIN_SCHEMA, handle_checkin)
39
57
  ctx.register_tool("antenna_check_matches", CHECK_MATCHES_SCHEMA, handle_check_matches)
40
58
  ctx.register_tool("antenna_bind", BIND_SCHEMA, handle_bind)
59
+ ctx.register_tool("antenna_pass", PASS_SCHEMA, handle_pass)
60
+ ctx.register_tool("antenna_discover", DISCOVER_SCHEMA, handle_discover)
61
+ ctx.register_tool("antenna_event_create", EVENT_CREATE_SCHEMA, handle_event_create)
62
+ ctx.register_tool("antenna_event_join", EVENT_JOIN_SCHEMA, handle_event_join)
63
+ ctx.register_tool("antenna_event_scan", EVENT_SCAN_SCHEMA, handle_event_scan)
41
64
 
42
65
  # ── Hook: auto-detect location + check web GPS events ─────────
43
66
  def on_pre_llm(messages, **kwargs):
44
- """Check for location data in messages AND pending web GPS events."""
45
- global _last_event_check
67
+ """Check for location data in messages AND pending web GPS events AND new matches."""
68
+ global _last_event_check, _last_match_check
46
69
  hints = []
47
70
 
71
+ now = time.time()
72
+
73
+ # 0. Check for new matches (every 60s)
74
+ if now - _last_match_check > _MATCH_CHECK_INTERVAL and _my_device_ids:
75
+ _last_match_check = now
76
+ try:
77
+ sb = _sb()
78
+ for did in list(_my_device_ids):
79
+ resp = sb.rpc("get_my_matches", {"p_device_id": did}).execute()
80
+ all_matches = resp.data or []
81
+ if not all_matches:
82
+ continue
83
+
84
+ incoming = [m for m in all_matches if m.get("device_id_b") == did]
85
+ my = [m for m in all_matches if m.get("device_id_a") == did]
86
+
87
+ for m in incoming:
88
+ key = f"{m['device_id_a']}→{did}"
89
+ if key in _notified_match_keys:
90
+ continue
91
+ _notified_match_keys.add(key)
92
+
93
+ # Get their profile
94
+ prof_resp = sb.rpc("get_profile", {"p_device_id": m["device_id_a"]}).execute()
95
+ p = prof_resp.data or {}
96
+ name = p.get("display_name") or "有人"
97
+ emoji = p.get("emoji") or "👤"
98
+
99
+ # Check if mutual
100
+ i_accepted = any(
101
+ x.get("device_id_b") == m["device_id_a"] for x in my
102
+ )
103
+ if i_accepted:
104
+ contact = m.get("contact_info_a")
105
+ contact_msg = f"对方的联系方式:{contact}" if contact else "对方还没有分享联系方式。"
106
+ hints.append(
107
+ f"[Antenna] 🎉 双向匹配!{emoji} {name} 也接受了你!{contact_msg} "
108
+ f"请立即通知用户这个好消息!"
109
+ )
110
+ else:
111
+ hints.append(
112
+ f"[Antenna] 📩 {emoji} {name} 想认识你!"
113
+ f"请告诉用户有人想认识 TA,并建议用 antenna_check_matches 查看详情。"
114
+ )
115
+ except Exception:
116
+ pass
117
+
48
118
  # 1. Check location_events table (web GPS updates)
49
119
  now = time.time()
50
120
  if now - _last_event_check > _EVENT_CHECK_INTERVAL:
@@ -134,3 +134,89 @@ BIND_SCHEMA = {
134
134
  "required": ["sender_id", "channel"],
135
135
  },
136
136
  }
137
+
138
+ PASS_SCHEMA = {
139
+ "name": "antenna_pass",
140
+ "description": "Pass/skip a person. They won't be recommended again.",
141
+ "parameters": {
142
+ "type": "object",
143
+ "properties": {
144
+ "sender_id": {"type": "string", "description": "The sender's user ID"},
145
+ "channel": {"type": "string", "description": "Platform name"},
146
+ "ref": {
147
+ "type": "string",
148
+ "description": "Ref number from scan/discover results (e.g. '1')",
149
+ },
150
+ "target_device_id": {
151
+ "type": "string",
152
+ "description": "Device ID (use ref instead when possible)",
153
+ },
154
+ },
155
+ "required": ["sender_id", "channel"],
156
+ },
157
+ }
158
+
159
+ DISCOVER_SCHEMA = {
160
+ "name": "antenna_discover",
161
+ "description": (
162
+ "Get today's global recommendation — the person most similar to you "
163
+ "worldwide. 1 per day, no repeats."
164
+ ),
165
+ "parameters": {
166
+ "type": "object",
167
+ "properties": {
168
+ "sender_id": {"type": "string", "description": "The sender's user ID"},
169
+ "channel": {"type": "string", "description": "Platform name"},
170
+ },
171
+ "required": ["sender_id", "channel"],
172
+ },
173
+ }
174
+
175
+ EVENT_CREATE_SCHEMA = {
176
+ "name": "antenna_event_create",
177
+ "description": (
178
+ "Create an event. Returns a shareable link (antenna.fyi/e/CODE) "
179
+ "for participants to join."
180
+ ),
181
+ "parameters": {
182
+ "type": "object",
183
+ "properties": {
184
+ "name": {"type": "string", "description": "Event name"},
185
+ "sender_id": {"type": "string", "description": "The sender's user ID"},
186
+ "channel": {"type": "string", "description": "Platform name"},
187
+ "lat": {"type": "number", "description": "Event latitude"},
188
+ "lng": {"type": "number", "description": "Event longitude"},
189
+ "starts_at": {"type": "string", "description": "Start time ISO"},
190
+ "ends_at": {"type": "string", "description": "End time ISO"},
191
+ },
192
+ "required": ["name", "sender_id", "channel"],
193
+ },
194
+ }
195
+
196
+ EVENT_JOIN_SCHEMA = {
197
+ "name": "antenna_event_join",
198
+ "description": "Join an event by its code from the event URL.",
199
+ "parameters": {
200
+ "type": "object",
201
+ "properties": {
202
+ "code": {"type": "string", "description": "Event code"},
203
+ "sender_id": {"type": "string", "description": "The sender's user ID"},
204
+ "channel": {"type": "string", "description": "Platform name"},
205
+ },
206
+ "required": ["code", "sender_id", "channel"],
207
+ },
208
+ }
209
+
210
+ EVENT_SCAN_SCHEMA = {
211
+ "name": "antenna_event_scan",
212
+ "description": "Scan people in an event. No distance limit.",
213
+ "parameters": {
214
+ "type": "object",
215
+ "properties": {
216
+ "code": {"type": "string", "description": "Event code"},
217
+ "sender_id": {"type": "string", "description": "The sender's user ID"},
218
+ "channel": {"type": "string", "description": "Platform name"},
219
+ },
220
+ "required": ["code", "sender_id", "channel"],
221
+ },
222
+ }
@@ -8,6 +8,7 @@ import json
8
8
  import math
9
9
  import os
10
10
  import time
11
+ import urllib.request
11
12
 
12
13
  try:
13
14
  from supabase import create_client
@@ -29,6 +30,7 @@ _client_url = None
29
30
  _last_scan: dict[str, float] = {}
30
31
  SCAN_DEBOUNCE_S = 30
31
32
  _last_ref_map: dict[str, str] = {} # ref → device_id from last scan
33
+ _my_device_ids: set[str] = set() # track this user's device_ids for match checking
32
34
 
33
35
 
34
36
  def _get_url():
@@ -53,7 +55,9 @@ def _sb():
53
55
 
54
56
 
55
57
  def _device_id(sender_id: str, channel: str) -> str:
56
- return f"{channel}:{sender_id}"
58
+ did = f"{channel}:{sender_id}"
59
+ _my_device_ids.add(did)
60
+ return did
57
61
 
58
62
 
59
63
  def _fuzzy(lat: float, lng: float) -> tuple[float, float]:
@@ -274,6 +278,173 @@ def handle_check_matches(params: dict) -> str:
274
278
  BASE_URL = "https://www.antenna.fyi"
275
279
 
276
280
 
281
+ def handle_pass(params: dict) -> str:
282
+ sb = _sb()
283
+ did = _device_id(params["sender_id"], params["channel"])
284
+
285
+ ref = params.get("ref")
286
+ target = params.get("target_device_id")
287
+ if ref and ref in _last_ref_map:
288
+ target = _last_ref_map[ref]
289
+ if not target and ref:
290
+ # Try resolve via RPC
291
+ try:
292
+ resp = sb.rpc("resolve_ref", {"p_device_id": did, "p_ref": ref}).execute()
293
+ if resp.data:
294
+ target = resp.data.get("target_device_id")
295
+ except Exception:
296
+ pass
297
+ if not target:
298
+ return _ok({"error": "No target. Use 'ref' from scan/discover results or 'target_device_id'."})
299
+
300
+ sb.rpc("pass_user", {
301
+ "p_device_id": did,
302
+ "p_target_device_id": target,
303
+ }).execute()
304
+
305
+ return _ok({"passed": True, "message": "已跳过,不会再推荐这个人了。"})
306
+
307
+
308
+ def handle_discover(params: dict) -> str:
309
+ sb = _sb()
310
+ did = _device_id(params["sender_id"], params["channel"])
311
+
312
+ resp = sb.rpc("global_discover", {"p_device_id": did}).execute()
313
+ results = resp.data or []
314
+
315
+ if not results:
316
+ return _ok({"count": 0, "message": "今天没有新的全球推荐了,明天再来看看。"})
317
+
318
+ global _last_ref_map
319
+ _last_ref_map = {}
320
+ profiles = []
321
+
322
+ # Get my profile for match reason
323
+ my_prof = sb.rpc("get_profile", {"p_device_id": did}).execute()
324
+ my_data = my_prof.data or {}
325
+ my_lines = [my_data.get("line1", ""), my_data.get("line2", ""), my_data.get("line3", "")]
326
+
327
+ for i, p in enumerate(results):
328
+ ref = str(i + 1)
329
+ _last_ref_map[ref] = p.get("device_id")
330
+
331
+ their_lines = [p.get("line1", ""), p.get("line2", ""), p.get("line3", "")]
332
+
333
+ # Generate match reason via Edge Function
334
+ match_reason = None
335
+ try:
336
+ req = urllib.request.Request(
337
+ f"{BUILTIN_URL}/functions/v1/generate-match-reason",
338
+ data=json.dumps({"my_lines": my_lines, "their_lines": their_lines}).encode(),
339
+ headers={"Content-Type": "application/json", "Authorization": f"Bearer {BUILTIN_KEY}"},
340
+ )
341
+ res = urllib.request.urlopen(req, timeout=10)
342
+ body = json.loads(res.read().decode())
343
+ match_reason = body.get("reason")
344
+ except Exception:
345
+ pass
346
+
347
+ profile = {
348
+ "ref": ref,
349
+ "emoji": p.get("emoji") or "\ud83d\udc64",
350
+ "name": p.get("display_name") or "匿名",
351
+ "line1": p.get("line1"),
352
+ "line2": p.get("line2"),
353
+ "line3": p.get("line3"),
354
+ }
355
+ if match_reason:
356
+ profile["match_reason"] = match_reason
357
+ profiles.append(profile)
358
+
359
+ return _ok({
360
+ "count": len(profiles),
361
+ "profiles": profiles,
362
+ "instruction": "这是全球推荐。根据你对用户的了解,判断是否值得推荐,写一句个性化的匹配理由。使用 ref 编号引用。",
363
+ })
364
+
365
+
366
+ def handle_event_create(params: dict) -> str:
367
+ sb = _sb()
368
+ did = _device_id(params["sender_id"], params["channel"])
369
+
370
+ rpc_params = {
371
+ "p_device_id": did,
372
+ "p_name": params["name"],
373
+ }
374
+ if params.get("lat") is not None:
375
+ rpc_params["p_lat"] = params["lat"]
376
+ if params.get("lng") is not None:
377
+ rpc_params["p_lng"] = params["lng"]
378
+ if params.get("starts_at"):
379
+ rpc_params["p_starts_at"] = params["starts_at"]
380
+ if params.get("ends_at"):
381
+ rpc_params["p_ends_at"] = params["ends_at"]
382
+
383
+ resp = sb.rpc("create_event", rpc_params).execute()
384
+ data = resp.data or {}
385
+
386
+ code = data.get("code", "")
387
+ return _ok({
388
+ "created": True,
389
+ "name": params["name"],
390
+ "code": code,
391
+ "url": f"{BASE_URL}/e/{code}",
392
+ "message": f"活动已创建!分享链接给参加的人:{BASE_URL}/e/{code}",
393
+ })
394
+
395
+
396
+ def handle_event_join(params: dict) -> str:
397
+ sb = _sb()
398
+ did = _device_id(params["sender_id"], params["channel"])
399
+
400
+ resp = sb.rpc("join_event", {
401
+ "p_device_id": did,
402
+ "p_code": params["code"],
403
+ }).execute()
404
+ data = resp.data or {}
405
+
406
+ if data.get("joined"):
407
+ return _ok({"joined": True, "name": data.get("name", ""), "message": f"已加入活动 \"{data.get('name', '')}\"!"})
408
+ return _ok({"joined": False, "error": data.get("error", "加入失败")})
409
+
410
+
411
+ def handle_event_scan(params: dict) -> str:
412
+ sb = _sb()
413
+ did = _device_id(params["sender_id"], params["channel"])
414
+
415
+ resp = sb.rpc("event_participants_list", {
416
+ "p_code": params["code"],
417
+ }).execute()
418
+ results = resp.data or []
419
+
420
+ others = [p for p in results if p.get("device_id") != did]
421
+
422
+ if not others:
423
+ return _ok({"count": 0, "profiles": [], "message": "活动里还没有其他人。"})
424
+
425
+ global _last_ref_map
426
+ _last_ref_map = {}
427
+ profiles = []
428
+ for i, p in enumerate(others):
429
+ ref = str(i + 1)
430
+ _last_ref_map[ref] = p.get("device_id")
431
+ profiles.append({
432
+ "ref": ref,
433
+ "emoji": p.get("emoji") or "\ud83d\udc64",
434
+ "name": p.get("display_name") or "匿名",
435
+ "line1": p.get("line1"),
436
+ "line2": p.get("line2"),
437
+ "line3": p.get("line3"),
438
+ "source": "event",
439
+ })
440
+
441
+ return _ok({
442
+ "count": len(profiles),
443
+ "profiles": profiles,
444
+ "instruction": "这些是活动参加者。根据你对用户的了解,推荐值得认识的人。使用 ref 编号引用。",
445
+ })
446
+
447
+
277
448
  def handle_bind(params: dict) -> str:
278
449
  sb = _sb()
279
450
  did = _device_id(params["sender_id"], params["channel"])
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antenna-fyi",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "description": "Antenna — nearby people discovery. CLI + MCP server + OpenClaw skill & plugin, all in one package.",
5
5
  "type": "module",
6
6
  "bin": {
package/skill/SKILL.md CHANGED
@@ -122,6 +122,20 @@ Get today's global recommendation — the person most similar to you worldwide.
122
122
  - If all users have been recommended, returns a message saying "wait for new people"
123
123
  - Use this in the daily cron job, or when user asks "find someone interesting globally"
124
124
 
125
+ ### `antenna_pass`
126
+ Pass/skip a person. They won't be recommended again.
127
+ - `sender_id`, `channel`: from context
128
+ - `ref`: ref number from scan/discover results (e.g. '1')
129
+ - `target_device_id`: device ID (use ref instead when possible)
130
+ - Use when the user says "skip", "pass", "not interested", etc.
131
+
132
+ ### `antenna_checkin`
133
+ Check in at a location — update your position so others can find you when they scan.
134
+ - `lat`, `lng`: coordinates (required)
135
+ - `sender_id`, `channel`: from context
136
+ - `place_name`: optional name of the place
137
+ - Use when the user says "I'm at XX" or wants to be discoverable without scanning others
138
+
125
139
  ## Data Transparency — what Antenna sends
126
140
 
127
141
  Antenna only communicates with Supabase (bcudjloikmpcqwcptuyd.supabase.co) via HTTPS.