antenna-fyi 1.3.1 → 1.3.2

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/core.js CHANGED
@@ -146,7 +146,7 @@ export async function scan({ lat, lng, radius_m = 500, device_id, supabaseUrl, s
146
146
  radius_m,
147
147
  profiles: [],
148
148
  _ref_map: {},
149
- message: `附近暂时没人,今天的全球推荐已经用完了。明天再来!`,
149
+ message: `附近暂时没人,今天的全球推荐也用完了。明天再来!提示:Antenna 每天有 1 次全球推荐(antenna_discover),下次可以试试。`,
150
150
  };
151
151
  }
152
152
 
@@ -197,7 +197,7 @@ def register(ctx):
197
197
  if resp.data:
198
198
  for evt in resp.data:
199
199
  hints.append(
200
- f"[Antenna] 📡 用户 {evt['device_id']} 通过网页分享了位置 "
200
+ f"[Antenna] 📡 有用户通过网页分享了位置 "
201
201
  f"({evt['lat']}, {evt['lng']})。"
202
202
  f"请使用 antenna_scan 查看附近有谁。"
203
203
  )
@@ -23,8 +23,12 @@ SCAN_SCHEMA = {
23
23
  "type": "string",
24
24
  "description": "Platform name (any platform: telegram, discord, webchat, signal, slack, etc.)",
25
25
  },
26
+ "chat_id": {
27
+ "type": "string",
28
+ "description": "REQUIRED for notifications. Pass chat/channel ID from message context.",
29
+ },
26
30
  },
27
- "required": ["sender_id", "channel"],
31
+ "required": ["sender_id", "channel", "chat_id"],
28
32
  },
29
33
  }
30
34
 
@@ -51,8 +55,9 @@ PROFILE_SCHEMA = {
51
55
  "line2": {"type": "string", "description": "What you're into"},
52
56
  "line3": {"type": "string", "description": "What you're looking for"},
53
57
  "visible": {"type": "boolean", "description": "Visible to others"},
58
+ "matching_context": {"type": "string", "description": "Free-form context for AI matching (interests, goals, etc.)"},
54
59
  },
55
- "required": ["action", "sender_id", "channel"],
60
+ "required": ["action", "sender_id", "channel", "chat_id"],
56
61
  },
57
62
  }
58
63
 
@@ -81,7 +86,7 @@ ACCEPT_SCHEMA = {
81
86
  "description": "Contact info to share (e.g. 'WeChat: yi')",
82
87
  },
83
88
  },
84
- "required": ["sender_id", "channel"],
89
+ "required": ["sender_id", "channel", "chat_id"],
85
90
  },
86
91
  }
87
92
 
@@ -103,7 +108,7 @@ CHECKIN_SCHEMA = {
103
108
  "description": "Name of the place (optional)",
104
109
  },
105
110
  },
106
- "required": ["lat", "lng", "sender_id", "channel"],
111
+ "required": ["lat", "lng", "sender_id", "channel", "chat_id"],
107
112
  },
108
113
  }
109
114
 
@@ -119,7 +124,7 @@ CHECK_MATCHES_SCHEMA = {
119
124
  "channel": {"type": "string"},
120
125
  "chat_id": {"type": "string", "description": "REQUIRED for notifications. Pass chat/channel ID from message context."},
121
126
  },
122
- "required": ["sender_id", "channel"],
127
+ "required": ["sender_id", "channel", "chat_id"],
123
128
  },
124
129
  }
125
130
 
@@ -137,7 +142,7 @@ BIND_SCHEMA = {
137
142
  "purpose": {"type": "string", "description": "'profile' (default) or 'event'"},
138
143
  "event_code": {"type": "string", "description": "Event code (when purpose=event)"},
139
144
  },
140
- "required": ["sender_id", "channel"],
145
+ "required": ["sender_id", "channel", "chat_id"],
141
146
  },
142
147
  }
143
148
 
@@ -149,6 +154,7 @@ PASS_SCHEMA = {
149
154
  "properties": {
150
155
  "sender_id": {"type": "string", "description": "The sender's user ID"},
151
156
  "channel": {"type": "string", "description": "Platform name (any platform works)"},
157
+ "chat_id": {"type": "string", "description": "REQUIRED for notifications. Pass chat/channel ID from message context."},
152
158
  "ref": {
153
159
  "type": "string",
154
160
  "description": "Ref number from scan/discover results (e.g. '1')",
@@ -158,7 +164,7 @@ PASS_SCHEMA = {
158
164
  "description": "Device ID (use ref instead when possible)",
159
165
  },
160
166
  },
161
- "required": ["sender_id", "channel"],
167
+ "required": ["sender_id", "channel", "chat_id"],
162
168
  },
163
169
  }
164
170
 
@@ -173,8 +179,9 @@ DISCOVER_SCHEMA = {
173
179
  "properties": {
174
180
  "sender_id": {"type": "string", "description": "The sender's user ID"},
175
181
  "channel": {"type": "string", "description": "Platform name (any platform works)"},
182
+ "chat_id": {"type": "string", "description": "REQUIRED for notifications. Pass chat/channel ID from message context."},
176
183
  },
177
- "required": ["sender_id", "channel"],
184
+ "required": ["sender_id", "channel", "chat_id"],
178
185
  },
179
186
  }
180
187
 
@@ -198,8 +205,9 @@ EVENT_CREATE_SCHEMA = {
198
205
  "og_image": {"type": "string", "description": "OG image URL for social sharing"},
199
206
  "requires_approval": {"type": "boolean", "description": "Require host approval to join (default false)"},
200
207
  "screening_questions": {"type": "array", "items": {"type": "string"}, "description": "Screening questions for applicants"},
208
+ "chat_id": {"type": "string", "description": "REQUIRED for notifications. Pass chat/channel ID from message context."},
201
209
  },
202
- "required": ["name", "sender_id", "channel", "starts_at", "ends_at"],
210
+ "required": ["name", "sender_id", "channel", "starts_at", "ends_at", "chat_id"],
203
211
  },
204
212
  }
205
213
 
@@ -215,8 +223,9 @@ EVENT_JOIN_SCHEMA = {
215
223
  "lat": {"type": "number", "description": "Latitude (optional, for auto-checkin)"},
216
224
  "lng": {"type": "number", "description": "Longitude (optional, for auto-checkin)"},
217
225
  "application_context": {"type": "string", "description": "Application context from screening conversation"},
226
+ "chat_id": {"type": "string", "description": "REQUIRED for notifications. Pass chat/channel ID from message context."},
218
227
  },
219
- "required": ["code", "sender_id", "channel"],
228
+ "required": ["code", "sender_id", "channel", "chat_id"],
220
229
  },
221
230
  }
222
231
 
@@ -229,8 +238,9 @@ EVENT_SCAN_SCHEMA = {
229
238
  "code": {"type": "string", "description": "Event code"},
230
239
  "sender_id": {"type": "string", "description": "The sender's user ID"},
231
240
  "channel": {"type": "string", "description": "Platform name (any platform works)"},
241
+ "chat_id": {"type": "string", "description": "REQUIRED for notifications. Pass chat/channel ID from message context."},
232
242
  },
233
- "required": ["code", "sender_id", "channel"],
243
+ "required": ["code", "sender_id", "channel", "chat_id"],
234
244
  },
235
245
  }
236
246
 
@@ -243,8 +253,9 @@ EVENT_END_SCHEMA = {
243
253
  "code": {"type": "string", "description": "Event code"},
244
254
  "sender_id": {"type": "string", "description": "The sender's user ID"},
245
255
  "channel": {"type": "string", "description": "Platform name (any platform works)"},
256
+ "chat_id": {"type": "string", "description": "REQUIRED for notifications. Pass chat/channel ID from message context."},
246
257
  },
247
- "required": ["code", "sender_id", "channel"],
258
+ "required": ["code", "sender_id", "channel", "chat_id"],
248
259
  },
249
260
  }
250
261
 
@@ -273,8 +284,9 @@ EVENT_CHECKIN_SCHEMA = {
273
284
  "channel": {"type": "string", "description": "Platform name (any platform works)"},
274
285
  "lat": {"type": "number", "description": "Latitude (optional)"},
275
286
  "lng": {"type": "number", "description": "Longitude (optional)"},
287
+ "chat_id": {"type": "string", "description": "REQUIRED for notifications. Pass chat/channel ID from message context."},
276
288
  },
277
- "required": ["code", "sender_id", "channel"],
289
+ "required": ["code", "sender_id", "channel", "chat_id"],
278
290
  },
279
291
  }
280
292
 
@@ -296,7 +308,7 @@ EVENT_UPDATE_SCHEMA = {
296
308
  "starts_at": {"type": "string"},
297
309
  "ends_at": {"type": "string"},
298
310
  },
299
- "required": ["code", "sender_id", "channel"],
311
+ "required": ["code", "sender_id", "channel", "chat_id"],
300
312
  },
301
313
  }
302
314
 
@@ -312,7 +324,7 @@ EVENT_APPROVE_SCHEMA = {
312
324
  "chat_id": {"type": "string", "description": "REQUIRED for notifications. Pass chat/channel ID from message context."},
313
325
  "ref": {"type": "string"},
314
326
  },
315
- "required": ["code", "sender_id", "channel", "ref"],
327
+ "required": ["code", "sender_id", "channel", "ref", "chat_id"],
316
328
  },
317
329
  }
318
330
 
@@ -328,7 +340,7 @@ EVENT_REJECT_SCHEMA = {
328
340
  "chat_id": {"type": "string", "description": "REQUIRED for notifications. Pass chat/channel ID from message context."},
329
341
  "ref": {"type": "string"},
330
342
  },
331
- "required": ["code", "sender_id", "channel", "ref"],
343
+ "required": ["code", "sender_id", "channel", "ref", "chat_id"],
332
344
  },
333
345
  }
334
346
 
@@ -344,6 +356,6 @@ EVENT_ADD_HOST_SCHEMA = {
344
356
  "chat_id": {"type": "string", "description": "REQUIRED for notifications. Pass chat/channel ID from message context."},
345
357
  "ref": {"type": "string"},
346
358
  },
347
- "required": ["code", "sender_id", "channel", "ref"],
359
+ "required": ["code", "sender_id", "channel", "ref", "chat_id"],
348
360
  },
349
361
  }
@@ -85,7 +85,7 @@ def handle_scan(params: dict) -> str:
85
85
  # Rate limit
86
86
  now = time.time()
87
87
  if did in _last_scan and now - _last_scan[did] < SCAN_DEBOUNCE_S:
88
- return _ok({"nearby": [], "message": "刚刚才扫描过,稍等一会儿再试。", "rate_limited": True})
88
+ return _ok({"profiles": [], "message": "刚刚才扫描过,稍等一会儿再试。", "rate_limited": True})
89
89
  _last_scan[did] = now
90
90
 
91
91
  lat = params.get("lat")
@@ -99,7 +99,7 @@ def handle_scan(params: dict) -> str:
99
99
  lat = loc["lat"]
100
100
  lng = loc["lng"]
101
101
  else:
102
- return _ok({"nearby": [], "message": "还没有位置信息。请先通过链接分享位置,或者发送位置消息。"})
102
+ return _ok({"profiles": [], "message": "还没有位置信息。请先通过链接分享位置,或者发送位置消息。"})
103
103
 
104
104
  flat, flng = _fuzzy(lat, lng)
105
105
 
@@ -111,7 +111,7 @@ def handle_scan(params: dict) -> str:
111
111
  others = [p for p in (resp.data or []) if p.get("device_id") != did]
112
112
 
113
113
  if not others:
114
- return _ok({"nearby": [], "message": f"在 {radius}m 范围内没有发现其他人。"})
114
+ return _ok({"profiles": [], "message": f"在 {radius}m 范围内没有发现其他人。"})
115
115
 
116
116
  # Build ref mapping — never expose device_id to agent/user
117
117
  global _last_ref_map
@@ -131,8 +131,8 @@ def handle_scan(params: dict) -> str:
131
131
  })
132
132
 
133
133
  return _ok({
134
- "nearby": profiles,
135
- "total": len(others),
134
+ "profiles": profiles,
135
+ "count": len(others),
136
136
  "radius_m": radius,
137
137
  "instruction": "根据你对用户的了解,判断哪些人值得推荐,为每个推荐写一句个性化的匹配理由。使用 ref 编号(如 '1', '2')来引用人员,不要显示 device_id。",
138
138
  })
@@ -149,7 +149,7 @@ def handle_profile(params: dict) -> str:
149
149
  return _ok({"exists": True, "profile": resp.data})
150
150
 
151
151
  # set
152
- resp = sb.rpc("upsert_profile", {
152
+ rpc_params = {
153
153
  "p_device_id": did,
154
154
  "p_display_name": params.get("display_name"),
155
155
  "p_emoji": params.get("emoji"),
@@ -157,7 +157,10 @@ def handle_profile(params: dict) -> str:
157
157
  "p_line2": params.get("line2"),
158
158
  "p_line3": params.get("line3"),
159
159
  "p_visible": params.get("visible", True),
160
- }).execute()
160
+ }
161
+ if params.get("matching_context") is not None:
162
+ rpc_params["p_matching_context"] = params["matching_context"]
163
+ resp = sb.rpc("upsert_profile", rpc_params).execute()
161
164
 
162
165
  if resp.data:
163
166
  return _ok({"updated": True, "profile": resp.data, "next_step": "IMPORTANT: Now call antenna_bind to generate a GPS link for the user. Do not skip this."})
@@ -225,45 +228,37 @@ def handle_check_matches(params: dict) -> str:
225
228
  sb = _sb()
226
229
  did = _device_id(params["sender_id"], params["channel"], params.get("chat_id"))
227
230
 
228
- resp = sb.rpc("get_my_matches", {"p_device_id": did}).execute()
229
- all_matches = resp.data or []
231
+ resp = sb.rpc("get_my_matches_with_profiles", {"p_device_id": did}).execute()
232
+ data = resp.data or {}
230
233
 
231
- if not all_matches:
232
- return _ok({"mutual_matches": [], "incoming_accepts": [], "message": "目前没有进行中的匹配。"})
234
+ raw_mutual = data.get("mutual_matches") or []
235
+ raw_incoming = data.get("incoming_accepts") or []
233
236
 
234
- my = [m for m in all_matches if m.get("device_id_a") == did]
235
- incoming = [m for m in all_matches if m.get("device_id_b") == did]
237
+ if not raw_mutual and not raw_incoming:
238
+ return _ok({"mutual_matches": [], "incoming_accepts": [], "message": "目前没有进行中的匹配。"})
236
239
 
237
- # Mutual
238
240
  mutual = []
239
- for m in my:
240
- rev = next((i for i in incoming if i.get("device_id_a") == m.get("device_id_b")), None)
241
- if rev:
242
- prof = sb.rpc("get_profile", {"p_device_id": m["device_id_b"]}).execute()
243
- p = prof.data or {}
244
- mutual.append({
245
- "ref": str(len(mutual) + 1),
246
- "_device_id": m["device_id_b"],
247
- "name": p.get("display_name") or "匿名",
248
- "emoji": p.get("emoji") or "👤",
249
- "their_contact": rev.get("contact_info_a"),
250
- "you_shared": m.get("contact_info_a"),
251
- })
252
-
253
- # Incoming only
241
+ for i, m in enumerate(raw_mutual):
242
+ mutual.append({
243
+ "ref": str(i + 1),
244
+ "_device_id": m.get("target_id"),
245
+ "name": m.get("name") or "匿名",
246
+ "emoji": m.get("emoji") or "👤",
247
+ "their_contact": m.get("their_contact"),
248
+ "you_shared": m.get("you_shared"),
249
+ })
250
+
254
251
  inc_only = []
255
- for m in incoming:
256
- already = next((x for x in my if x.get("device_id_b") == m.get("device_id_a")), None)
257
- if not already:
258
- prof = sb.rpc("get_profile", {"p_device_id": m["device_id_a"]}).execute()
259
- p = prof.data or {}
260
- inc_only.append({
261
- "ref": str(len(inc_only) + 1),
262
- "_device_id": m["device_id_a"],
263
- "name": p.get("display_name") or "匿名",
264
- "emoji": p.get("emoji") or "👤",
265
- "line1": p.get("line1"),
266
- })
252
+ for i, m in enumerate(raw_incoming):
253
+ inc_only.append({
254
+ "ref": str(i + 1),
255
+ "_device_id": m.get("target_id"),
256
+ "name": m.get("name") or "匿名",
257
+ "emoji": m.get("emoji") or "👤",
258
+ "line1": m.get("line1"),
259
+ "line2": m.get("line2"),
260
+ "line3": m.get("line3"),
261
+ })
267
262
 
268
263
  msgs = []
269
264
  if mutual:
@@ -294,7 +289,7 @@ def handle_pass(params: dict) -> str:
294
289
  if not target and ref:
295
290
  # Try resolve via RPC
296
291
  try:
297
- resp = sb.rpc("resolve_ref", {"p_device_id": did, "p_ref": ref}).execute()
292
+ resp = sb.rpc("resolve_ref", {"p_owner": did, "p_ref": ref}).execute()
298
293
  if resp.data:
299
294
  target = resp.data.get("target_device_id")
300
295
  except Exception:
@@ -304,7 +299,7 @@ def handle_pass(params: dict) -> str:
304
299
 
305
300
  sb.rpc("pass_user", {
306
301
  "p_device_id": did,
307
- "p_target_device_id": target,
302
+ "p_passed_device_id": target,
308
303
  }).execute()
309
304
 
310
305
  return _ok({"passed": True, "message": "已跳过,不会再推荐这个人了。"})
@@ -329,9 +324,11 @@ def handle_discover(params: dict) -> str:
329
324
  my_data = my_prof.data or {}
330
325
  my_lines = [my_data.get("line1", ""), my_data.get("line2", ""), my_data.get("line3", "")]
331
326
 
327
+ ref_map = {}
332
328
  for i, p in enumerate(results):
333
329
  ref = str(i + 1)
334
330
  _last_ref_map[ref] = p.get("device_id")
331
+ ref_map[ref] = p.get("device_id")
335
332
 
336
333
  their_lines = [p.get("line1", ""), p.get("line2", ""), p.get("line3", "")]
337
334
 
@@ -361,6 +358,17 @@ def handle_discover(params: dict) -> str:
361
358
  profile["match_reason"] = match_reason
362
359
  profiles.append(profile)
363
360
 
361
+ # Save refs and log recommendations
362
+ try:
363
+ sb.rpc("save_scan_refs", {"p_owner": did, "p_refs": ref_map}).execute()
364
+ except Exception:
365
+ pass
366
+ for p in results:
367
+ try:
368
+ sb.rpc("log_recommendation", {"p_device_id": did, "p_recommended_id": p["device_id"]}).execute()
369
+ except Exception:
370
+ pass
371
+
364
372
  return _ok({
365
373
  "count": len(profiles),
366
374
  "profiles": profiles,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antenna-fyi",
3
- "version": "1.3.1",
3
+ "version": "1.3.2",
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
@@ -230,7 +230,7 @@ Source code is open: https://github.com/H1an1/Antenna
230
230
  > "最后——你想被叫什么?再选个 emoji 代表你自己。"
231
231
 
232
232
  **第六步:确认**
233
- 把名片组装好,展示给用户确认:
233
+ 把名片组装好,**确认三行都有内容后**展示给用户确认:
234
234
  > 你的名片:
235
235
  >
236
236
  > 🎸 **小林**
@@ -240,7 +240,8 @@ Source code is open: https://github.com/H1an1/Antenna
240
240
  >
241
241
  > 看看有没有要改的?OK 的话我就存了。
242
242
 
243
- 用户说 OK → `antenna_profile` action="set" 保存。
243
+ 用户说 OK → **检查 line1、line2、line3 都不为空后** 调 `antenna_profile` action="set" 保存。
244
+ 如果某一行为空,先问用户补完再保存。后端会拒绝没有 line1 的新 profile,并对缺失的 line2/line3 返回 warning。
244
245
  用户说要改 → 改完再确认。
245
246
 
246
247
  **关键原则:**
@@ -253,6 +254,10 @@ Source code is open: https://github.com/H1an1/Antenna
253
254
 
254
255
  ### Showing results — 你来判断,不是服务器
255
256
 
257
+ **第一次 scan 的新用户:** 简短一句解释:"这是附近的人。Antenna 基于 AI 匹配,看到感兴趣的人 accept,双向匹配后交换联系方式。"
258
+
259
+ **Profile 不完整时:** 如果用户的 profile 只有 1 行,提示:"你的名片只填了一行,补完后匹配质量会更好。要现在补吗?"
260
+
256
261
  `antenna_scan` 返回的是附近所有人的名片,**没有打分、没有预匹配**。你需要:
257
262
 
258
263
  **全球推荐 fallback:** 如果 scan 结果里有 `global: true`,说明附近没人,这些是全球推荐。告诉用户“附近暂时没人,但全球有这几个有意思的人”,然后正常推荐。用户仍然可以 accept。