antenna-fyi 1.0.1 → 1.2.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 +122 -5
- package/lib/mcp.js +88 -0
- package/package.json +1 -1
- package/skill/SKILL.md +18 -0
package/lib/core.js
CHANGED
|
@@ -13,6 +13,28 @@ let _url = null;
|
|
|
13
13
|
// ─── Embedding ───────────────────────────────────────────────────────
|
|
14
14
|
|
|
15
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.`;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const res = await fetch(`${GEMINI_FLASH_URL}?key=${apiKey}`, {
|
|
26
|
+
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
|
+
}),
|
|
32
|
+
});
|
|
33
|
+
if (!res.ok) return null;
|
|
34
|
+
const data = await res.json();
|
|
35
|
+
return data?.candidates?.[0]?.content?.parts?.[0]?.text?.trim() || null;
|
|
36
|
+
} catch { return null; }
|
|
37
|
+
}
|
|
16
38
|
|
|
17
39
|
async function generateEmbedding(text) {
|
|
18
40
|
const apiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
|
|
@@ -382,20 +404,33 @@ export async function discover({ device_id, supabaseUrl, supabaseKey }) {
|
|
|
382
404
|
};
|
|
383
405
|
}
|
|
384
406
|
|
|
385
|
-
// Build ref map +
|
|
407
|
+
// Build ref map + generate match reasons
|
|
386
408
|
const _refMap = {};
|
|
387
|
-
const
|
|
409
|
+
const myProfile = await getProfile({ device_id, supabaseUrl, supabaseKey });
|
|
410
|
+
const myLines = myProfile ? [myProfile.line1, myProfile.line2, myProfile.line3].filter(Boolean).join(". ") : "";
|
|
411
|
+
|
|
412
|
+
const profiles = [];
|
|
413
|
+
for (let i = 0; i < results.length; i++) {
|
|
414
|
+
const p = results[i];
|
|
388
415
|
const ref = String(i + 1);
|
|
389
416
|
_refMap[ref] = p.device_id;
|
|
390
|
-
|
|
417
|
+
|
|
418
|
+
const theirLines = [p.line1, p.line2, p.line3].filter(Boolean).join(". ");
|
|
419
|
+
let reason = null;
|
|
420
|
+
if (myLines && theirLines) {
|
|
421
|
+
reason = await generateMatchReason(myLines, theirLines);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
profiles.push({
|
|
391
425
|
ref,
|
|
392
426
|
name: p.display_name || "匿名",
|
|
393
427
|
emoji: p.emoji || "👤",
|
|
394
428
|
line1: p.line1,
|
|
395
429
|
line2: p.line2,
|
|
396
430
|
line3: p.line3,
|
|
397
|
-
|
|
398
|
-
|
|
431
|
+
match_reason: reason,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
399
434
|
|
|
400
435
|
// Log who was recommended (for dedup)
|
|
401
436
|
for (const p of results) {
|
|
@@ -421,6 +456,88 @@ export async function discover({ device_id, supabaseUrl, supabaseKey }) {
|
|
|
421
456
|
};
|
|
422
457
|
}
|
|
423
458
|
|
|
459
|
+
// ─── pass ───────────────────────────────────────────────────────────
|
|
460
|
+
|
|
461
|
+
export async function pass({ device_id, target_device_id, ref, supabaseUrl, supabaseKey }) {
|
|
462
|
+
const sb = getClient(supabaseUrl, supabaseKey);
|
|
463
|
+
|
|
464
|
+
let targetId = target_device_id;
|
|
465
|
+
if (!targetId && ref && device_id) {
|
|
466
|
+
const { data } = await sb.rpc("resolve_ref", { p_owner: device_id, p_ref: ref });
|
|
467
|
+
targetId = data;
|
|
468
|
+
}
|
|
469
|
+
if (!targetId) {
|
|
470
|
+
return { passed: false, error: "No target. Ref may have expired — try scanning again." };
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
await sb.rpc("pass_user", { p_device_id: device_id, p_passed_device_id: targetId });
|
|
474
|
+
return { passed: true, message: "已跳过,下次不会再推荐这个人。" };
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// ─── events ─────────────────────────────────────────────────────────
|
|
478
|
+
|
|
479
|
+
export async function createEvent({ name, lat, lng, device_id, starts_at, ends_at, supabaseUrl, supabaseKey }) {
|
|
480
|
+
const sb = getClient(supabaseUrl, supabaseKey);
|
|
481
|
+
const { data, error } = await sb.rpc("create_event", {
|
|
482
|
+
p_name: name,
|
|
483
|
+
p_lat: lat || null,
|
|
484
|
+
p_lng: lng || null,
|
|
485
|
+
p_created_by: device_id || null,
|
|
486
|
+
p_starts_at: starts_at || new Date().toISOString(),
|
|
487
|
+
p_ends_at: ends_at || new Date(Date.now() + 12 * 60 * 60 * 1000).toISOString(),
|
|
488
|
+
});
|
|
489
|
+
if (error) throw new Error(error.message);
|
|
490
|
+
return data;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export async function joinEvent({ code, device_id, supabaseUrl, supabaseKey }) {
|
|
494
|
+
const sb = getClient(supabaseUrl, supabaseKey);
|
|
495
|
+
const { data, error } = await sb.rpc("join_event", { p_code: code, p_device_id: device_id });
|
|
496
|
+
if (error) throw new Error(error.message);
|
|
497
|
+
return data;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
export async function eventScan({ code, device_id, supabaseUrl, supabaseKey }) {
|
|
501
|
+
const sb = getClient(supabaseUrl, supabaseKey);
|
|
502
|
+
const { data, error } = await sb.rpc("event_participants_list", { p_code: code, p_device_id: device_id });
|
|
503
|
+
if (error) throw new Error(error.message);
|
|
504
|
+
|
|
505
|
+
const others = data || [];
|
|
506
|
+
const _refMap = {};
|
|
507
|
+
const profiles = others.map((p, i) => {
|
|
508
|
+
const ref = String(i + 1);
|
|
509
|
+
_refMap[ref] = p.device_id;
|
|
510
|
+
return {
|
|
511
|
+
ref,
|
|
512
|
+
name: p.display_name || "匿名",
|
|
513
|
+
emoji: p.emoji || "👤",
|
|
514
|
+
line1: p.line1,
|
|
515
|
+
line2: p.line2,
|
|
516
|
+
line3: p.line3,
|
|
517
|
+
source: "event",
|
|
518
|
+
};
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
// Persist refs
|
|
522
|
+
if (device_id && Object.keys(_refMap).length > 0) {
|
|
523
|
+
try { await sb.rpc("save_scan_refs", { p_owner: device_id, p_refs: JSON.stringify(_refMap) }); } catch {}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return {
|
|
527
|
+
count: profiles.length,
|
|
528
|
+
profiles,
|
|
529
|
+
_ref_map: _refMap,
|
|
530
|
+
event: true,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export async function getEvent({ code, supabaseUrl, supabaseKey }) {
|
|
535
|
+
const sb = getClient(supabaseUrl, supabaseKey);
|
|
536
|
+
const { data, error } = await sb.rpc("get_event", { p_code: code });
|
|
537
|
+
if (error) throw new Error(error.message);
|
|
538
|
+
return data;
|
|
539
|
+
}
|
|
540
|
+
|
|
424
541
|
export async function createBindToken({ device_id, supabaseUrl, supabaseKey }) {
|
|
425
542
|
const sb = getClient(supabaseUrl, supabaseKey);
|
|
426
543
|
const { data, error } = await sb.rpc("create_bind_token", { p_device_id: device_id });
|
package/lib/mcp.js
CHANGED
|
@@ -8,10 +8,14 @@ import {
|
|
|
8
8
|
getProfile,
|
|
9
9
|
setProfile,
|
|
10
10
|
accept,
|
|
11
|
+
pass,
|
|
11
12
|
checkMatches,
|
|
12
13
|
checkin,
|
|
13
14
|
createBindToken,
|
|
14
15
|
discover,
|
|
16
|
+
createEvent,
|
|
17
|
+
joinEvent,
|
|
18
|
+
eventScan,
|
|
15
19
|
deriveDeviceId,
|
|
16
20
|
} from "./core.js";
|
|
17
21
|
|
|
@@ -194,6 +198,90 @@ export async function startMcpServer() {
|
|
|
194
198
|
}
|
|
195
199
|
);
|
|
196
200
|
|
|
201
|
+
// ─── antenna_pass ────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
server.tool(
|
|
204
|
+
"antenna_pass",
|
|
205
|
+
"Pass/skip a person. They won't be recommended again.",
|
|
206
|
+
{
|
|
207
|
+
sender_id: z.string().describe("The sender's user ID"),
|
|
208
|
+
channel: z.string().describe("Channel name"),
|
|
209
|
+
ref: z.string().optional().describe("Ref number from scan/discover results"),
|
|
210
|
+
target_device_id: z.string().optional().describe("Device ID (use ref instead)"),
|
|
211
|
+
},
|
|
212
|
+
async ({ sender_id, channel, ref, target_device_id }) => {
|
|
213
|
+
try {
|
|
214
|
+
const result = await pass({ device_id: deriveDeviceId(sender_id, channel), target_device_id, ref });
|
|
215
|
+
return jsonResult(result);
|
|
216
|
+
} catch (e) {
|
|
217
|
+
return jsonResult({ error: e.message });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
// ─── antenna_event_create ──────────────────────────────────
|
|
223
|
+
|
|
224
|
+
server.tool(
|
|
225
|
+
"antenna_event_create",
|
|
226
|
+
"Create an event. Returns a shareable link (antenna.fyi/e/CODE) for participants to join.",
|
|
227
|
+
{
|
|
228
|
+
name: z.string().describe("Event name"),
|
|
229
|
+
sender_id: z.string().describe("Creator's user ID"),
|
|
230
|
+
channel: z.string().describe("Channel name"),
|
|
231
|
+
lat: z.number().optional().describe("Event latitude"),
|
|
232
|
+
lng: z.number().optional().describe("Event longitude"),
|
|
233
|
+
starts_at: z.string().optional().describe("Start time ISO string"),
|
|
234
|
+
ends_at: z.string().optional().describe("End time ISO string"),
|
|
235
|
+
},
|
|
236
|
+
async ({ name, sender_id, channel, lat, lng, starts_at, ends_at }) => {
|
|
237
|
+
try {
|
|
238
|
+
const result = await createEvent({ name, lat, lng, device_id: deriveDeviceId(sender_id, channel), starts_at, ends_at });
|
|
239
|
+
return jsonResult(result);
|
|
240
|
+
} catch (e) { return jsonResult({ error: e.message }); }
|
|
241
|
+
}
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
// ─── antenna_event_join ────────────────────────────────────
|
|
245
|
+
|
|
246
|
+
server.tool(
|
|
247
|
+
"antenna_event_join",
|
|
248
|
+
"Join an event by its code. The code is from the event URL (antenna.fyi/e/CODE).",
|
|
249
|
+
{
|
|
250
|
+
code: z.string().describe("Event code"),
|
|
251
|
+
sender_id: z.string().describe("The sender's user ID"),
|
|
252
|
+
channel: z.string().describe("Channel name"),
|
|
253
|
+
},
|
|
254
|
+
async ({ code, sender_id, channel }) => {
|
|
255
|
+
try {
|
|
256
|
+
const result = await joinEvent({ code, device_id: deriveDeviceId(sender_id, channel) });
|
|
257
|
+
return jsonResult(result);
|
|
258
|
+
} catch (e) { return jsonResult({ error: e.message }); }
|
|
259
|
+
}
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
// ─── antenna_event_scan ────────────────────────────────────
|
|
263
|
+
|
|
264
|
+
server.tool(
|
|
265
|
+
"antenna_event_scan",
|
|
266
|
+
"Scan people in an event. No distance limit — returns all event participants.",
|
|
267
|
+
{
|
|
268
|
+
code: z.string().describe("Event code"),
|
|
269
|
+
sender_id: z.string().describe("The sender's user ID"),
|
|
270
|
+
channel: z.string().describe("Channel name"),
|
|
271
|
+
},
|
|
272
|
+
async ({ code, sender_id, channel }) => {
|
|
273
|
+
try {
|
|
274
|
+
const result = await eventScan({ code, device_id: deriveDeviceId(sender_id, channel) });
|
|
275
|
+
if (result._ref_map) {
|
|
276
|
+
_lastRefMap = { ..._lastRefMap, ...result._ref_map };
|
|
277
|
+
const { _ref_map, ...clean } = result;
|
|
278
|
+
return jsonResult(clean);
|
|
279
|
+
}
|
|
280
|
+
return jsonResult(result);
|
|
281
|
+
} catch (e) { return jsonResult({ error: e.message }); }
|
|
282
|
+
}
|
|
283
|
+
);
|
|
284
|
+
|
|
197
285
|
const transport = new StdioServerTransport();
|
|
198
286
|
await server.connect(transport);
|
|
199
287
|
}
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -251,3 +251,21 @@ Plugin 自带后台服务,每 10 分钟轮询一次 Supabase 查新的 mutual
|
|
|
251
251
|
3. 如果对方分享了联系方式,一并展示
|
|
252
252
|
|
|
253
253
|
用户不需要主动问,agent 会自动收到通知。
|
|
254
|
+
|
|
255
|
+
### `antenna_event_create`
|
|
256
|
+
Create an event. Returns a shareable link (antenna.fyi/e/CODE).
|
|
257
|
+
- `name`: event name
|
|
258
|
+
- `sender_id`, `channel`: from context
|
|
259
|
+
- `lat`, `lng`: optional event location
|
|
260
|
+
- `starts_at`, `ends_at`: optional time range (default: now to +12h)
|
|
261
|
+
|
|
262
|
+
### `antenna_event_join`
|
|
263
|
+
Join an event by code.
|
|
264
|
+
- `code`: from the event URL (antenna.fyi/e/CODE)
|
|
265
|
+
- `sender_id`, `channel`: from context
|
|
266
|
+
|
|
267
|
+
### `antenna_event_scan`
|
|
268
|
+
Scan people in an event. No distance limit — returns all participants.
|
|
269
|
+
- `code`: event code
|
|
270
|
+
- `sender_id`, `channel`: from context
|
|
271
|
+
- Returns profiles with `source: "event"` tag
|