antenna-fyi 0.12.2 → 0.14.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/lib/core.js CHANGED
@@ -109,6 +109,15 @@ export async function scan({ lat, lng, radius_m = 500, device_id, supabaseUrl, s
109
109
  };
110
110
  });
111
111
 
112
+ // Save refs to DB (persist across agent restarts)
113
+ const saveRefs = async (refMap) => {
114
+ if (device_id && Object.keys(refMap).length > 0) {
115
+ try {
116
+ await sb.rpc("save_scan_refs", { p_owner: device_id, p_refs: JSON.stringify(refMap) });
117
+ } catch { /* best effort */ }
118
+ }
119
+ };
120
+
112
121
  // If nobody nearby, fallback to global discover (1 per day)
113
122
  if (others.length === 0 && device_id) {
114
123
  const { data: globalData } = await sb.rpc("global_discover", {
@@ -117,10 +126,12 @@ export async function scan({ lat, lng, radius_m = 500, device_id, supabaseUrl, s
117
126
  });
118
127
  const globalOthers = globalData || [];
119
128
  if (globalOthers.length > 0) {
129
+ const profs = buildProfiles(globalOthers);
130
+ await saveRefs(_refMap);
120
131
  return {
121
132
  count: globalOthers.length,
122
133
  radius_m,
123
- profiles: buildProfiles(globalOthers),
134
+ profiles: profs,
124
135
  _ref_map: _refMap,
125
136
  global: true,
126
137
  message: `附近 ${radius_m}m 暂时没人。今天的全球推荐——从这 ${globalOthers.length} 个人里挑一个最匹配的推荐给用户。(每天 1 次)`,
@@ -135,10 +146,13 @@ export async function scan({ lat, lng, radius_m = 500, device_id, supabaseUrl, s
135
146
  };
136
147
  }
137
148
 
149
+ const profs = buildProfiles(others);
150
+ await saveRefs(_refMap);
151
+
138
152
  return {
139
153
  count: others.length,
140
154
  radius_m,
141
- profiles: buildProfiles(others),
155
+ profiles: profs,
142
156
  _ref_map: _refMap,
143
157
  };
144
158
  }
@@ -202,15 +216,26 @@ export async function setProfile({
202
216
  export async function accept({
203
217
  device_id,
204
218
  target_device_id,
219
+ ref,
205
220
  contact_info,
206
221
  supabaseUrl,
207
222
  supabaseKey,
208
223
  }) {
209
224
  const sb = getClient(supabaseUrl, supabaseKey);
210
225
 
226
+ // Resolve ref from DB if target_device_id not provided
227
+ let targetId = target_device_id;
228
+ if (!targetId && ref && device_id) {
229
+ const { data } = await sb.rpc("resolve_ref", { p_owner: device_id, p_ref: ref });
230
+ targetId = data;
231
+ }
232
+ if (!targetId) {
233
+ return { accepted: false, error: "No target. Ref may have expired — try scanning again." };
234
+ }
235
+
211
236
  const { error } = await sb.rpc("upsert_match", {
212
237
  p_device_id_a: device_id,
213
- p_device_id_b: target_device_id,
238
+ p_device_id_b: targetId,
214
239
  p_reason: "",
215
240
  p_score: 0,
216
241
  p_status: "accepted",
@@ -223,7 +248,7 @@ export async function accept({
223
248
  const { data: reverse } = await sb
224
249
  .from("matches")
225
250
  .select("status, contact_info_a")
226
- .eq("device_id_a", target_device_id)
251
+ .eq("device_id_a", targetId)
227
252
  .eq("device_id_b", device_id)
228
253
  .eq("status", "accepted")
229
254
  .single();
@@ -337,6 +362,65 @@ export async function checkMatches({ device_id, supabaseUrl, supabaseKey }) {
337
362
 
338
363
  // ─── createBindToken ─────────────────────────────────────────────
339
364
 
365
+ // ─── discover (global recommendation) ─────────────────────────────
366
+
367
+ export async function discover({ device_id, supabaseUrl, supabaseKey }) {
368
+ const sb = getClient(supabaseUrl, supabaseKey);
369
+
370
+ const { data: globalData } = await sb.rpc("global_discover", {
371
+ p_device_id: device_id,
372
+ p_limit: 1,
373
+ });
374
+
375
+ const results = globalData || [];
376
+ if (results.length === 0) {
377
+ // Check if all used up or daily limit
378
+ return {
379
+ count: 0,
380
+ profiles: [],
381
+ message: "今天的全球推荐已用完,或者你已经看过所有人了。等新人加入!",
382
+ };
383
+ }
384
+
385
+ // Build ref map + persist to DB
386
+ const _refMap = {};
387
+ const profiles = results.map((p, i) => {
388
+ const ref = String(i + 1);
389
+ _refMap[ref] = p.device_id;
390
+ return {
391
+ ref,
392
+ name: p.display_name || "匿名",
393
+ emoji: p.emoji || "👤",
394
+ line1: p.line1,
395
+ line2: p.line2,
396
+ line3: p.line3,
397
+ };
398
+ });
399
+
400
+ // Log who was recommended (for dedup)
401
+ for (const p of results) {
402
+ await sb.rpc("log_recommendation", {
403
+ p_device_id: device_id,
404
+ p_recommended_id: p.device_id,
405
+ });
406
+ }
407
+
408
+ // Persist ref map to DB
409
+ if (device_id && Object.keys(_refMap).length > 0) {
410
+ try {
411
+ await sb.rpc("save_scan_refs", { p_owner: device_id, p_refs: JSON.stringify(_refMap) });
412
+ } catch { /* best effort */ }
413
+ }
414
+
415
+ return {
416
+ count: profiles.length,
417
+ profiles,
418
+ _ref_map: _refMap,
419
+ global: true,
420
+ message: `🌍 今天的全球推荐——这个人跟你可能聊得来。`,
421
+ };
422
+ }
423
+
340
424
  export async function createBindToken({ device_id, supabaseUrl, supabaseKey }) {
341
425
  const sb = getClient(supabaseUrl, supabaseKey);
342
426
  const { data, error } = await sb.rpc("create_bind_token", { p_device_id: device_id });
package/lib/mcp.js CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  checkMatches,
12
12
  checkin,
13
13
  createBindToken,
14
+ discover,
14
15
  deriveDeviceId,
15
16
  } from "./core.js";
16
17
 
@@ -98,14 +99,7 @@ export async function startMcpServer() {
98
99
  },
99
100
  async ({ sender_id, channel, ref, target_device_id, contact_info }) => {
100
101
  try {
101
- let targetId = target_device_id;
102
- if (ref && _lastRefMap[ref]) {
103
- targetId = _lastRefMap[ref];
104
- }
105
- if (!targetId) {
106
- return jsonResult({ error: "No target specified. Use 'ref' from scan results or 'target_device_id'." });
107
- }
108
- const result = await accept({ device_id: deriveDeviceId(sender_id, channel), target_device_id: targetId, contact_info });
102
+ const result = await accept({ device_id: deriveDeviceId(sender_id, channel), target_device_id, ref, contact_info });
109
103
  return jsonResult(result);
110
104
  } catch (e) {
111
105
  return jsonResult({ error: e.message });
@@ -176,6 +170,30 @@ export async function startMcpServer() {
176
170
  }
177
171
  );
178
172
 
173
+ // ─── antenna_discover ────────────────────────────────────────
174
+
175
+ server.tool(
176
+ "antenna_discover",
177
+ "Get today's global recommendation — the person most similar to you worldwide. 1 per day, no repeats.",
178
+ {
179
+ sender_id: z.string().describe("The sender's user ID"),
180
+ channel: z.string().describe("Channel name"),
181
+ },
182
+ async ({ sender_id, channel }) => {
183
+ try {
184
+ const result = await discover({ device_id: deriveDeviceId(sender_id, channel) });
185
+ if (result._ref_map) {
186
+ _lastRefMap = result._ref_map;
187
+ const { _ref_map, ...clean } = result;
188
+ return jsonResult(clean);
189
+ }
190
+ return jsonResult(result);
191
+ } catch (e) {
192
+ return jsonResult({ error: e.message });
193
+ }
194
+ }
195
+ );
196
+
179
197
  const transport = new StdioServerTransport();
180
198
  await server.connect(transport);
181
199
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antenna-fyi",
3
- "version": "0.12.2",
3
+ "version": "0.14.0",
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
@@ -43,9 +43,17 @@ onboarding 完成后,帮用户设置一个每小时自动检查匹配的 cron
43
43
 
44
44
  示例(Hermes):
45
45
  ```
46
- hermes cron add --every 1h --message "Check antenna matches: call antenna_check_matches. If there are new mutual matches or incoming accepts, tell the user. If nothing new, reply HEARTBEAT_OK."
46
+ hermes cron add --every 1h --message "Check antenna matches: call antenna_check_matches. If there are new mutual matches or incoming accepts, tell the user. Also call antenna_discover once per day to get a global recommendation. If nothing new, reply HEARTBEAT_OK."
47
47
  ```
48
48
 
49
+ **第五步:问用户要不要全球推荐**
50
+
51
+ cron 设完后,问用户一句:
52
+ > "要我每天帮你找一个全球最像你的人吗?"
53
+
54
+ 用户说要 → cron 每天调 `antenna_discover` 推送。
55
+ 用户说不要 → 只在 scan 没人时 fallback。
56
+
49
57
  ## When to use
50
58
 
51
59
  - **首次安装后**: 主动开始 onboarding(名片 → 位置)
@@ -101,6 +109,13 @@ Generate a GPS binding link. **You MUST call this immediately after saving a pro
101
109
  - Send this link to the user — they open it on their phone, allow GPS, and their location is automatically shared
102
110
  - **MANDATORY after profile save. Do not wait for user to ask.**
103
111
 
112
+ ### `antenna_discover`
113
+ Get today's global recommendation — the person most similar to you worldwide. 1 per day, no repeats.
114
+ - `sender_id`, `channel`: from context
115
+ - Returns 1 profile (embedding similarity match) that hasn't been recommended before
116
+ - If all users have been recommended, returns a message saying "wait for new people"
117
+ - Use this in the daily cron job, or when user asks "find someone interesting globally"
118
+
104
119
  ## Behavior guidelines
105
120
 
106
121
  ### First-time user — 聊天式引导(不要让用户填表)