antenna-openclaw-plugin 1.3.27 → 1.3.29
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 +419 -263
- package/migrations/drift_bottles.sql +177 -0
- package/openclaw.plugin.json +2 -2
- package/package.json +2 -2
- package/skills/antenna/SKILL.md +271 -228
- package/tests/drift-bottle.test.ts +109 -0
- package/skills/antenna/EVENTS.md +0 -163
package/index.ts
CHANGED
|
@@ -25,17 +25,17 @@ interface Profile {
|
|
|
25
25
|
line1: string | null;
|
|
26
26
|
line2: string | null;
|
|
27
27
|
line3: string | null;
|
|
28
|
-
emoji: string | null;
|
|
29
|
-
profile_slug: string | null;
|
|
30
|
-
matching_context: string | null;
|
|
31
28
|
visible: boolean;
|
|
32
29
|
last_seen_at?: string;
|
|
30
|
+
profile_slug?: string;
|
|
31
|
+
distance_m?: number;
|
|
32
|
+
dist_meters?: number;
|
|
33
|
+
matching_context?: string;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
interface MatchResult {
|
|
36
37
|
device_id: string;
|
|
37
38
|
display_name: string | null;
|
|
38
|
-
emoji: string | null;
|
|
39
39
|
line1: string | null;
|
|
40
40
|
line2: string | null;
|
|
41
41
|
line3: string | null;
|
|
@@ -58,7 +58,7 @@ function getConfig(api: any): AntennaConfig {
|
|
|
58
58
|
supabaseUrl: cfg.supabaseUrl || BUILTIN_SUPABASE_URL,
|
|
59
59
|
supabaseKey: cfg.supabaseKey || BUILTIN_SUPABASE_ANON_KEY,
|
|
60
60
|
defaultRadiusM: cfg.defaultRadiusM ?? 500,
|
|
61
|
-
matchExpiryHours: cfg.matchExpiryHours ??
|
|
61
|
+
matchExpiryHours: cfg.matchExpiryHours ?? 24,
|
|
62
62
|
maxMatches: cfg.maxMatches ?? 5,
|
|
63
63
|
autoScanOnLocation: cfg.autoScanOnLocation ?? true,
|
|
64
64
|
};
|
|
@@ -102,10 +102,10 @@ function extractWords(profile: Partial<Profile>): string[] {
|
|
|
102
102
|
return text.split(/[\s,,。.!!??、;;::]+/).filter((w) => w.length > 1);
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
function deriveDeviceId(senderId: string, channel: string, chatId?: string): string {
|
|
105
|
+
function deriveDeviceId(senderId: string, channel: string, chatId?: string, api?: any): string {
|
|
106
106
|
const id = `${channel}:${senderId}`;
|
|
107
107
|
_knownDeviceIds.add(id);
|
|
108
|
-
if (chatId) {
|
|
108
|
+
if (chatId && api) {
|
|
109
109
|
_channelContext.set(id, chatId);
|
|
110
110
|
// Persist to DB async
|
|
111
111
|
try {
|
|
@@ -140,12 +140,13 @@ async function notifyUser(
|
|
|
140
140
|
userId: string,
|
|
141
141
|
message: string,
|
|
142
142
|
logger: any,
|
|
143
|
+
api?: any,
|
|
143
144
|
): Promise<void> {
|
|
144
145
|
const deviceId = `${channel}:${userId}`;
|
|
145
146
|
let chatId = _channelContext.get(deviceId);
|
|
146
147
|
|
|
147
148
|
// Fallback: read from DB if not in memory
|
|
148
|
-
if (!chatId) {
|
|
149
|
+
if (!chatId && api) {
|
|
149
150
|
try {
|
|
150
151
|
const cfg = getConfig(api);
|
|
151
152
|
const sb = getSupabase(cfg);
|
|
@@ -275,7 +276,7 @@ export default function register(api: any) {
|
|
|
275
276
|
async execute(_id: string, params: any) {
|
|
276
277
|
const cfg = getConfig(api);
|
|
277
278
|
const supabase = getSupabase(cfg);
|
|
278
|
-
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
|
|
279
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id, api);
|
|
279
280
|
const radius = params.radius_m ?? cfg.defaultRadiusM ?? 500;
|
|
280
281
|
|
|
281
282
|
if (isRateLimited(deviceId)) {
|
|
@@ -307,29 +308,10 @@ export default function register(api: any) {
|
|
|
307
308
|
const others = (nearby ?? []).filter((p: Profile) => p.device_id !== deviceId);
|
|
308
309
|
|
|
309
310
|
if (others.length === 0) {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
311
|
+
return ok({
|
|
312
|
+
profiles: [], count: 0, radius_m: radius,
|
|
313
|
+
message: `附近 ${radius}m 暂时没人。可以试试全球推荐(antenna_discover)或者把你的名片链接发给想认识的人。`,
|
|
313
314
|
});
|
|
314
|
-
const globalOthers = globalData || [];
|
|
315
|
-
if (globalOthers.length > 0) {
|
|
316
|
-
const gRefMap: Record<string, string> = {};
|
|
317
|
-
const gProfiles = globalOthers.map((p: any, i: number) => {
|
|
318
|
-
const ref = String(i + 1);
|
|
319
|
-
gRefMap[ref] = p.device_id;
|
|
320
|
-
return { ref, emoji: p.emoji || "👤", name: p.display_name || "匿名", line1: p.line1, line2: p.line2, line3: p.line3, more_information: p.matching_context || null, profile_slug: p.profile_slug || null };
|
|
321
|
-
});
|
|
322
|
-
(api as any)._antennaRefMap = gRefMap;
|
|
323
|
-
try { await supabase.rpc("save_scan_refs", { p_owner: deviceId, p_refs: gRefMap }); } catch {}
|
|
324
|
-
for (const p of globalOthers) {
|
|
325
|
-
try { await supabase.rpc("log_recommendation", { p_device_id: deviceId, p_recommended_id: p.device_id }); } catch {}
|
|
326
|
-
}
|
|
327
|
-
return ok({
|
|
328
|
-
profiles: gProfiles, count: gProfiles.length, radius_m: radius, global: true,
|
|
329
|
-
message: `附近 ${radius}m 暂时没人。今天的全球推荐——这个人跟你可能聊得来。(每天 1 次)`,
|
|
330
|
-
});
|
|
331
|
-
}
|
|
332
|
-
return ok({ profiles: [], message: `附近暂时没人,今天的全球推荐已经用完了。明天再来!` });
|
|
333
315
|
}
|
|
334
316
|
|
|
335
317
|
// Build ref mapping — never expose device_id
|
|
@@ -339,12 +321,10 @@ export default function register(api: any) {
|
|
|
339
321
|
_refMap[ref] = p.device_id;
|
|
340
322
|
return {
|
|
341
323
|
ref,
|
|
342
|
-
emoji: p.emoji || "👤",
|
|
343
324
|
name: p.display_name || "匿名",
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
more_information: p.matching_context || null,
|
|
325
|
+
personal_description: p.line1,
|
|
326
|
+
looking_for: p.line2,
|
|
327
|
+
conversation_style: p.line3,
|
|
348
328
|
profile_slug: p.profile_slug || null,
|
|
349
329
|
distance_m: p.distance_m ?? p.dist_meters ?? null,
|
|
350
330
|
};
|
|
@@ -371,7 +351,7 @@ export default function register(api: any) {
|
|
|
371
351
|
api.registerTool({
|
|
372
352
|
name: "antenna_profile",
|
|
373
353
|
description:
|
|
374
|
-
"View or update the user's Antenna profile (name card). The profile has a display name
|
|
354
|
+
"View or update the user's Antenna profile (name card). The profile has a display name and three descriptions: personal description, looking for, and conversation style.",
|
|
375
355
|
parameters: {
|
|
376
356
|
type: "object",
|
|
377
357
|
properties: {
|
|
@@ -380,45 +360,98 @@ export default function register(api: any) {
|
|
|
380
360
|
channel: { type: "string", description: "The channel name" },
|
|
381
361
|
chat_id: { type: "string", description: "REQUIRED for notifications. Pass the chat/channel ID from your message context so Antenna can send you match and event notifications." },
|
|
382
362
|
display_name: { type: "string", description: "Display name" },
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
line3: { type: "string", description: "Third line (what you're looking for)" },
|
|
363
|
+
personal_description: { type: "string", description: "Personal description — who you are and what you do (max 220 chars)" },
|
|
364
|
+
looking_for: { type: "string", description: "Looking for — the kind of people you want to meet (max 140 chars)" },
|
|
365
|
+
conversation_style: { type: "string", description: "Conversation style — the type of conversations you want (max 160 chars)" },
|
|
387
366
|
visible: { type: "boolean", description: "Whether to be visible to others" },
|
|
388
|
-
|
|
367
|
+
more_information: { type: "string", description: "More information — agent-generated rich context for better matching (not shown to others, max 1000 chars). Generate this FIRST, then derive personal_description, looking_for, and conversation_style from it." },
|
|
368
|
+
interest_tags: { type: "array", items: { type: "string" }, description: "Interest/topic tags shown on the card (up to 8)" },
|
|
369
|
+
city: { type: "string", description: "Country or region (e.g. 'United States', 'Beijing')" },
|
|
370
|
+
links: { type: "array", items: { type: "string" }, description: "Social links shown on the card footer (up to 3)" },
|
|
371
|
+
is_active: { type: "boolean", description: "Whether the profile is active or quiet" },
|
|
372
|
+
contact_info: { type: "string", description: "Contact info (WeChat, Telegram, email, etc.) — PRIVATE, only revealed to mutual matches. Ask the user what contact they want to share." },
|
|
389
373
|
},
|
|
390
374
|
required: ["action", "sender_id", "channel", "chat_id"],
|
|
391
375
|
},
|
|
392
376
|
async execute(_id: string, params: any) {
|
|
393
377
|
const cfg = getConfig(api);
|
|
394
378
|
const supabase = getSupabase(cfg);
|
|
395
|
-
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
|
|
379
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id, api);
|
|
396
380
|
|
|
397
381
|
if (params.action === "get") {
|
|
398
382
|
const { data, error } = await supabase.rpc("get_profile", { p_device_id: deviceId });
|
|
399
383
|
if (error || !data) {
|
|
400
|
-
return ok({
|
|
384
|
+
return ok({
|
|
385
|
+
exists: false,
|
|
386
|
+
message: "你还没有名片。跟我聊聊你是谁、做什么、想认识什么人,我帮你创建。",
|
|
387
|
+
fields: {
|
|
388
|
+
display_name: { label: "显示名称", description: "How you want to be called" },
|
|
389
|
+
personal_description: { label: "个人描述", description: "Who you are and what you do", maxLength: 220, required: true },
|
|
390
|
+
looking_for: { label: "想认识的人", description: "The kind of people you want to meet", maxLength: 140 },
|
|
391
|
+
conversation_style: { label: "想要的交流方式", description: "The type of conversations you want", maxLength: 160 },
|
|
392
|
+
more_information: { label: "更多信息", description: "Agent-generated rich context for better matching (not shown to others)", maxLength: 1000 },
|
|
393
|
+
interest_tags: { label: "兴趣标签", description: "Interest/topic tags shown on the card (up to 8)", maxItems: 8 },
|
|
394
|
+
city: { label: "国家/地区", description: "Country or region" },
|
|
395
|
+
links: { label: "社交链接", description: "Social links shown on the card footer (up to 3)", maxItems: 3 },
|
|
396
|
+
is_active: { label: "状态", description: "Whether the profile is active or quiet" },
|
|
397
|
+
},
|
|
398
|
+
});
|
|
401
399
|
}
|
|
402
400
|
return ok({
|
|
403
401
|
exists: true,
|
|
404
|
-
profile: { display_name: data.display_name,
|
|
405
|
-
|
|
402
|
+
profile: { display_name: data.display_name,
|
|
403
|
+
personal_description: data.line1, looking_for: data.line2, conversation_style: data.line3, visible: data.visible, contact_info: data.contact_info || null },
|
|
404
|
+
fields: {
|
|
405
|
+
display_name: { label: "显示名称", description: "How you want to be called" },
|
|
406
|
+
personal_description: { label: "个人描述", description: "Who you are and what you do", maxLength: 220, required: true },
|
|
407
|
+
looking_for: { label: "想认识的人", description: "The kind of people you want to meet", maxLength: 140 },
|
|
408
|
+
conversation_style: { label: "想要的交流方式", description: "The type of conversations you want", maxLength: 160 },
|
|
409
|
+
more_information: { label: "更多信息", description: "Agent-generated rich context for better matching (not shown to others)", maxLength: 1000 },
|
|
410
|
+
interest_tags: { label: "兴趣标签", description: "Interest/topic tags shown on the card (up to 8)", maxItems: 8 },
|
|
411
|
+
city: { label: "国家/地区", description: "Country or region" },
|
|
412
|
+
links: { label: "社交链接", description: "Social links shown on the card footer (up to 3)", maxItems: 3 },
|
|
413
|
+
is_active: { label: "状态", description: "Whether the profile is active or quiet" },
|
|
414
|
+
},
|
|
406
415
|
});
|
|
407
416
|
}
|
|
408
417
|
|
|
418
|
+
// Pack structured fields into matching_context JSON
|
|
419
|
+
let contextJson = params.more_information ?? undefined;
|
|
420
|
+
if (params.interest_tags || params.city || params.links || params.is_active !== undefined) {
|
|
421
|
+
let existing: Record<string, any> = {};
|
|
422
|
+
if (contextJson) {
|
|
423
|
+
try { existing = JSON.parse(contextJson); } catch { existing = { context: contextJson }; }
|
|
424
|
+
} else {
|
|
425
|
+
// Read existing matching_context from DB to merge
|
|
426
|
+
try {
|
|
427
|
+
const { data: cur } = await supabase.rpc("get_profile", { p_device_id: deviceId });
|
|
428
|
+
if (cur?.matching_context) {
|
|
429
|
+
try { existing = JSON.parse(cur.matching_context); } catch {}
|
|
430
|
+
}
|
|
431
|
+
} catch {}
|
|
432
|
+
}
|
|
433
|
+
if (params.interest_tags) existing.interestTags = params.interest_tags;
|
|
434
|
+
if (params.city) existing.city = params.city;
|
|
435
|
+
if (params.links) existing.links = params.links;
|
|
436
|
+
if (params.is_active !== undefined) existing.isActive = params.is_active;
|
|
437
|
+
existing.version = existing.version || 1;
|
|
438
|
+
contextJson = JSON.stringify(existing);
|
|
439
|
+
}
|
|
440
|
+
|
|
409
441
|
const { data, error } = await supabase.rpc("upsert_profile", {
|
|
410
442
|
p_device_id: deviceId,
|
|
411
|
-
p_display_name: params.display_name ?? null, p_emoji:
|
|
412
|
-
p_line1: params.
|
|
413
|
-
p_line3: params.
|
|
414
|
-
...(
|
|
443
|
+
p_display_name: params.display_name ?? null, p_emoji: null,
|
|
444
|
+
p_line1: params.personal_description ?? null, p_line2: params.looking_for ?? null,
|
|
445
|
+
p_line3: params.conversation_style ?? null, p_visible: params.visible ?? true,
|
|
446
|
+
...(contextJson != null ? { p_matching_context: contextJson } : {}),
|
|
447
|
+
...(params.contact_info != null ? { p_contact_info: params.contact_info } : {}),
|
|
415
448
|
});
|
|
416
449
|
|
|
417
450
|
if (error) return ok({ error: error.message });
|
|
418
451
|
|
|
419
452
|
// Read back profile to get slug for public page link
|
|
420
|
-
let publicUrl = null;
|
|
421
|
-
let
|
|
453
|
+
let publicUrl: string | null = null;
|
|
454
|
+
let gpsBindUrl: string | null = null;
|
|
422
455
|
try {
|
|
423
456
|
const { data: profile } = await supabase.rpc("get_profile", { p_device_id: deviceId });
|
|
424
457
|
if (profile?.profile_slug) {
|
|
@@ -426,50 +459,25 @@ export default function register(api: any) {
|
|
|
426
459
|
}
|
|
427
460
|
} catch {}
|
|
428
461
|
|
|
429
|
-
//
|
|
462
|
+
// Auto-generate GPS bind link
|
|
430
463
|
try {
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
Apollo: ["music", "media", "content", "creator", "writing", "taste"],
|
|
439
|
-
Artemis: ["independent", "explore", "freelance", "health", "outdoor"],
|
|
440
|
-
Aphrodite: ["beauty", "brand", "fashion", "relationship"],
|
|
441
|
-
Dionysus: ["event", "culture", "party", "art", "festival"],
|
|
442
|
-
Hades: ["finance", "invest", "infrastructure", "backend", "security"],
|
|
443
|
-
Persephone: ["transform", "cross", "research", "academic", "bridge"],
|
|
444
|
-
Odysseus: ["founder", "journey", "resilience", "travel", "startup"],
|
|
445
|
-
};
|
|
446
|
-
let bestRole = "Prometheus"; let bestScore = 0;
|
|
447
|
-
for (const [role, kws] of Object.entries(archetypeKw)) {
|
|
448
|
-
const score = kws.reduce((s, kw) => s + (corpus.includes(kw) ? 1 : 0), 0);
|
|
449
|
-
if (score > bestScore) { bestScore = score; bestRole = role; }
|
|
450
|
-
}
|
|
451
|
-
const cfg2 = getConfig(api);
|
|
452
|
-
const supabaseUrl = cfg2.supabaseUrl || "https://bcudjloikmpcqwcptuyd.supabase.co";
|
|
453
|
-
const supabaseKey = cfg2.supabaseKey || BUILTIN_SUPABASE_ANON_KEY;
|
|
454
|
-
const res = await fetch(`${supabaseUrl}/functions/v1/generate-archetype`, {
|
|
455
|
-
method: "POST",
|
|
456
|
-
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${supabaseKey}` },
|
|
457
|
-
body: JSON.stringify({ archetype: bestRole, profile_text: profileText }),
|
|
458
|
-
});
|
|
459
|
-
if (res.ok) {
|
|
460
|
-
const archData = await res.json();
|
|
461
|
-
if (archData?.reason) archetypeResult = { archetype: bestRole, ...archData };
|
|
462
|
-
}
|
|
464
|
+
const { data: bindData } = await supabase.rpc("create_bind_token", {
|
|
465
|
+
p_device_id: deviceId,
|
|
466
|
+
p_purpose: "profile",
|
|
467
|
+
p_event_code: null,
|
|
468
|
+
});
|
|
469
|
+
if (bindData?.token) {
|
|
470
|
+
gpsBindUrl = `https://www.antenna.fyi/locate?token=${bindData.token}`;
|
|
463
471
|
}
|
|
464
472
|
} catch {}
|
|
465
473
|
|
|
466
474
|
return ok({
|
|
467
475
|
updated: true,
|
|
468
476
|
profile: { display_name: data.display_name,
|
|
469
|
-
|
|
477
|
+
personal_description: data.line1, looking_for: data.line2, conversation_style: data.line3, visible: data.visible },
|
|
470
478
|
public_url: publicUrl,
|
|
471
|
-
|
|
472
|
-
next_step: "
|
|
479
|
+
gps_bind_url: gpsBindUrl,
|
|
480
|
+
next_step: "Send the public_url and gps_bind_url to the user. The GPS link should be opened on their phone to share location.",
|
|
473
481
|
});
|
|
474
482
|
},
|
|
475
483
|
});
|
|
@@ -496,7 +504,7 @@ export default function register(api: any) {
|
|
|
496
504
|
async execute(_id: string, params: any) {
|
|
497
505
|
const cfg = getConfig(api);
|
|
498
506
|
const supabase = getSupabase(cfg);
|
|
499
|
-
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
|
|
507
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id, api);
|
|
500
508
|
const fuzzy = fuzzyCoords(params.lat, params.lng);
|
|
501
509
|
|
|
502
510
|
// Check if user has a profile first
|
|
@@ -504,7 +512,7 @@ export default function register(api: any) {
|
|
|
504
512
|
if (!profile) {
|
|
505
513
|
return ok({
|
|
506
514
|
checked_in: false,
|
|
507
|
-
message: "
|
|
515
|
+
message: "你还没有名片,别人看到你也不知道你是谁。先创建一个名片吧(跟我聊聊你是谁、做什么、想认识什么人)。",
|
|
508
516
|
});
|
|
509
517
|
}
|
|
510
518
|
|
|
@@ -528,7 +536,7 @@ export default function register(api: any) {
|
|
|
528
536
|
api.registerTool({
|
|
529
537
|
name: "antenna_accept",
|
|
530
538
|
description:
|
|
531
|
-
"Accept a match. Use 'ref' from scan results (e.g. '1', '2'), target_device_id, or profile_slug (from
|
|
539
|
+
"Accept a match. Use 'ref' from scan results (e.g. '1', '2'), target_device_id, or profile_slug (from profile links like antenna.fyi/p/xxx). Optionally share contact info.",
|
|
532
540
|
parameters: {
|
|
533
541
|
type: "object",
|
|
534
542
|
properties: {
|
|
@@ -536,8 +544,8 @@ export default function register(api: any) {
|
|
|
536
544
|
channel: { type: "string" },
|
|
537
545
|
chat_id: { type: "string", description: "REQUIRED for notifications. Pass the chat/channel ID from your message context so Antenna can send you match and event notifications." },
|
|
538
546
|
ref: { type: "string", description: "Ref number from scan results (e.g. '1')" },
|
|
539
|
-
target_device_id: { type: "string", description: "Device ID (use ref
|
|
540
|
-
profile_slug: { type: "string", description: "Profile slug from
|
|
547
|
+
target_device_id: { type: "string", description: "Device ID (use ref instead when possible)" },
|
|
548
|
+
profile_slug: { type: "string", description: "Profile slug from profile link (e.g. 'yi' from antenna.fyi/p/yi)" },
|
|
541
549
|
contact_info: { type: "string", description: "Optional contact info to share" },
|
|
542
550
|
},
|
|
543
551
|
required: ["sender_id", "channel", "chat_id"],
|
|
@@ -545,7 +553,7 @@ export default function register(api: any) {
|
|
|
545
553
|
async execute(_id: string, params: any) {
|
|
546
554
|
const cfg = getConfig(api);
|
|
547
555
|
const supabase = getSupabase(cfg);
|
|
548
|
-
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
|
|
556
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id, api);
|
|
549
557
|
|
|
550
558
|
// Resolve ref to device_id — try DB first, then memory fallback
|
|
551
559
|
let targetId = params.target_device_id;
|
|
@@ -554,12 +562,13 @@ export default function register(api: any) {
|
|
|
554
562
|
const { data: resolved } = await supabase.rpc("resolve_ref", { p_owner: deviceId, p_ref: params.ref });
|
|
555
563
|
targetId = resolved || (api as any)._antennaRefMap?.[params.ref];
|
|
556
564
|
}
|
|
557
|
-
// Resolve profile_slug to device_id
|
|
565
|
+
// Resolve profile_slug to device_id
|
|
558
566
|
if (!targetId && params.profile_slug) {
|
|
559
|
-
const { data:
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
567
|
+
const { data: slugData } = await supabase.rpc("get_profile_by_slug", { p_slug: params.profile_slug });
|
|
568
|
+
if (slugData?.found) {
|
|
569
|
+
targetId = slugData.device_id;
|
|
570
|
+
} else {
|
|
571
|
+
return ok({ error: `Profile slug "${params.profile_slug}" not found.` });
|
|
563
572
|
}
|
|
564
573
|
}
|
|
565
574
|
if (!targetId) {
|
|
@@ -629,7 +638,7 @@ export default function register(api: any) {
|
|
|
629
638
|
async execute(_id: string, params: any) {
|
|
630
639
|
const cfg = getConfig(api);
|
|
631
640
|
const supabase = getSupabase(cfg);
|
|
632
|
-
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
|
|
641
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id, api);
|
|
633
642
|
|
|
634
643
|
const { data, error } = await supabase.rpc("create_bind_token", {
|
|
635
644
|
p_device_id: deviceId,
|
|
@@ -651,45 +660,6 @@ export default function register(api: any) {
|
|
|
651
660
|
},
|
|
652
661
|
});
|
|
653
662
|
|
|
654
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
655
|
-
// Tool: antenna_link_account
|
|
656
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
657
|
-
api.registerTool({
|
|
658
|
-
name: "antenna_link_account",
|
|
659
|
-
description:
|
|
660
|
-
"Link your Antenna agent profile to your antenna.fyi website account. Pass the user's API key — the server verifies it and extracts the user_id. The agent never needs to know or pass user_id directly.",
|
|
661
|
-
parameters: {
|
|
662
|
-
type: "object",
|
|
663
|
-
properties: {
|
|
664
|
-
sender_id: { type: "string", description: "The sender's user ID" },
|
|
665
|
-
channel: { type: "string", description: "The channel name" },
|
|
666
|
-
chat_id: { type: "string", description: "REQUIRED. Pass the chat/channel ID from your message context." },
|
|
667
|
-
api_key: { type: "string", description: "The user's Antenna API key (ant_xxx) from antenna.fyi/me" },
|
|
668
|
-
},
|
|
669
|
-
required: ["sender_id", "channel", "chat_id", "api_key"],
|
|
670
|
-
},
|
|
671
|
-
async execute(_id: string, params: any) {
|
|
672
|
-
const cfg = getConfig(api);
|
|
673
|
-
const supabase = getSupabase(cfg);
|
|
674
|
-
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
|
|
675
|
-
|
|
676
|
-
const { data, error } = await supabase.rpc("bind_user_id", {
|
|
677
|
-
p_device_id: deviceId,
|
|
678
|
-
p_api_key: params.api_key,
|
|
679
|
-
});
|
|
680
|
-
if (error) return ok({ error: error.message });
|
|
681
|
-
|
|
682
|
-
if (data?.error) {
|
|
683
|
-
return ok(data);
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
return ok({
|
|
687
|
-
...data,
|
|
688
|
-
message: "账号已关联!现在你可以在 antenna.fyi/me 看到你的完整 profile 和匹配记录了。",
|
|
689
|
-
});
|
|
690
|
-
},
|
|
691
|
-
});
|
|
692
|
-
|
|
693
663
|
// ═══════════════════════════════════════════════════════════════════
|
|
694
664
|
// Tool: antenna_discover
|
|
695
665
|
// ═══════════════════════════════════════════════════════════════════
|
|
@@ -709,7 +679,7 @@ export default function register(api: any) {
|
|
|
709
679
|
async execute(_id: string, params: any) {
|
|
710
680
|
const cfg = getConfig(api);
|
|
711
681
|
const supabase = getSupabase(cfg);
|
|
712
|
-
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
|
|
682
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id, api);
|
|
713
683
|
|
|
714
684
|
const { data: globalData } = await supabase.rpc("global_discover", {
|
|
715
685
|
p_device_id: deviceId, p_limit: 1,
|
|
@@ -752,7 +722,7 @@ export default function register(api: any) {
|
|
|
752
722
|
} catch { /* best effort */ }
|
|
753
723
|
}
|
|
754
724
|
|
|
755
|
-
profiles.push({ ref,
|
|
725
|
+
profiles.push({ ref, name: p.display_name || "匿名", personal_description: p.line1, looking_for: p.line2, conversation_style: p.line3, profile_slug: p.profile_slug || null, match_reason });
|
|
756
726
|
}
|
|
757
727
|
|
|
758
728
|
// Persist refs + log recommendation
|
|
@@ -777,7 +747,7 @@ export default function register(api: any) {
|
|
|
777
747
|
api.registerTool({
|
|
778
748
|
name: "antenna_initial_recommendations",
|
|
779
749
|
description:
|
|
780
|
-
"
|
|
750
|
+
"First-time recommendations — show 2-3 best matches right after profile creation. One-time only (second call returns empty). Does NOT consume daily discover quota. Use in onboarding step 3.",
|
|
781
751
|
parameters: {
|
|
782
752
|
type: "object",
|
|
783
753
|
properties: {
|
|
@@ -790,27 +760,51 @@ export default function register(api: any) {
|
|
|
790
760
|
async execute(_id: string, params: any) {
|
|
791
761
|
const cfg = getConfig(api);
|
|
792
762
|
const supabase = getSupabase(cfg);
|
|
793
|
-
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
|
|
763
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id, api);
|
|
794
764
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
765
|
+
// Check if already done via profile matching_context
|
|
766
|
+
const { data: myProfileCheck } = await supabase.rpc("get_profile", { p_device_id: deviceId });
|
|
767
|
+
if (myProfileCheck?.matching_context) {
|
|
768
|
+
try {
|
|
769
|
+
const ctx = JSON.parse(myProfileCheck.matching_context);
|
|
770
|
+
if (ctx.initialRecommendationsDone) {
|
|
771
|
+
return ok({ count: 0, profiles: [], message: "首次推荐已经用过了。用 antenna_discover 获取每日推荐。" });
|
|
772
|
+
}
|
|
773
|
+
} catch {}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const { data: globalData } = await supabase.rpc("global_discover", {
|
|
777
|
+
p_device_id: deviceId, p_limit: 3,
|
|
798
778
|
});
|
|
799
779
|
|
|
800
|
-
|
|
780
|
+
const results = globalData || [];
|
|
801
781
|
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
782
|
+
// Mark initial recommendations as done in matching_context
|
|
783
|
+
try {
|
|
784
|
+
let ctx: Record<string, any> = {};
|
|
785
|
+
if (myProfileCheck?.matching_context) {
|
|
786
|
+
try { ctx = JSON.parse(myProfileCheck.matching_context); } catch {}
|
|
787
|
+
}
|
|
788
|
+
ctx.initialRecommendationsDone = true;
|
|
789
|
+
await supabase.rpc("upsert_profile", {
|
|
790
|
+
p_device_id: deviceId,
|
|
791
|
+
p_display_name: myProfileCheck?.display_name ?? null,
|
|
792
|
+
p_emoji: null,
|
|
793
|
+
p_line1: myProfileCheck?.line1 ?? null,
|
|
794
|
+
p_line2: myProfileCheck?.line2 ?? null,
|
|
795
|
+
p_line3: myProfileCheck?.line3 ?? null,
|
|
796
|
+
p_visible: myProfileCheck?.visible ?? true,
|
|
797
|
+
p_matching_context: JSON.stringify(ctx),
|
|
808
798
|
});
|
|
799
|
+
} catch {}
|
|
800
|
+
|
|
801
|
+
if (results.length === 0) {
|
|
802
|
+
return ok({ count: 0, profiles: [], message: "目前还没有足够的用户来匹配。你是早期用户!" });
|
|
809
803
|
}
|
|
810
804
|
|
|
811
805
|
const _refMap: Record<string, string> = {};
|
|
812
806
|
|
|
813
|
-
// Get my profile for match reason
|
|
807
|
+
// Get my profile for match reason
|
|
814
808
|
const { data: myProfile } = await supabase.rpc("get_profile", { p_device_id: deviceId });
|
|
815
809
|
const myLines = myProfile ? [myProfile.line1, myProfile.line2, myProfile.line3].filter(Boolean).join(". ") : "";
|
|
816
810
|
|
|
@@ -839,21 +833,21 @@ export default function register(api: any) {
|
|
|
839
833
|
} catch { /* best effort */ }
|
|
840
834
|
}
|
|
841
835
|
|
|
842
|
-
profiles.push({
|
|
836
|
+
profiles.push({
|
|
837
|
+
ref, name: p.display_name || "匿名",
|
|
838
|
+
personal_description: p.line1, looking_for: p.line2, conversation_style: p.line3, profile_slug: p.profile_slug || null, match_reason,
|
|
839
|
+
});
|
|
843
840
|
}
|
|
844
841
|
|
|
845
|
-
// Persist refs + log recommendations
|
|
846
842
|
(api as any)._antennaRefMap = { ...(api as any)._antennaRefMap, ..._refMap };
|
|
847
|
-
try {
|
|
848
|
-
await supabase.rpc("save_scan_refs", { p_owner: deviceId, p_refs: _refMap });
|
|
849
|
-
} catch { /* best effort */ }
|
|
843
|
+
try { await supabase.rpc("save_scan_refs", { p_owner: deviceId, p_refs: _refMap }); } catch {}
|
|
850
844
|
for (const p of results) {
|
|
851
|
-
await supabase.rpc("log_recommendation", { p_device_id: deviceId, p_recommended_id:
|
|
845
|
+
await supabase.rpc("log_recommendation", { p_device_id: deviceId, p_recommended_id: p.device_id }).catch(() => {});
|
|
852
846
|
}
|
|
853
847
|
|
|
854
848
|
return ok({
|
|
855
849
|
count: profiles.length, profiles, initial: true,
|
|
856
|
-
message: "
|
|
850
|
+
message: "\uD83C\uDF1F 这是你的首次推荐——跟你最像的人:",
|
|
857
851
|
});
|
|
858
852
|
},
|
|
859
853
|
});
|
|
@@ -885,11 +879,11 @@ export default function register(api: any) {
|
|
|
885
879
|
async execute(_id: string, params: any) {
|
|
886
880
|
const cfg = getConfig(api);
|
|
887
881
|
const supabase = getSupabase(cfg);
|
|
888
|
-
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
|
|
882
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id, api);
|
|
889
883
|
const { data, error } = await supabase.rpc("create_event", {
|
|
890
884
|
p_name: params.name,
|
|
891
|
-
p_lat: params.lat
|
|
892
|
-
p_lng: params.lng
|
|
885
|
+
p_lat: params.lat || null,
|
|
886
|
+
p_lng: params.lng || null,
|
|
893
887
|
p_created_by: deviceId,
|
|
894
888
|
p_starts_at: params.starts_at || null,
|
|
895
889
|
p_ends_at: params.ends_at || null,
|
|
@@ -922,7 +916,7 @@ export default function register(api: any) {
|
|
|
922
916
|
async execute(_id: string, params: any) {
|
|
923
917
|
const cfg = getConfig(api);
|
|
924
918
|
const supabase = getSupabase(cfg);
|
|
925
|
-
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
|
|
919
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id, api);
|
|
926
920
|
const { data, error } = await supabase.rpc("end_event", {
|
|
927
921
|
p_code: params.code,
|
|
928
922
|
p_device_id: deviceId,
|
|
@@ -954,7 +948,7 @@ export default function register(api: any) {
|
|
|
954
948
|
async execute(_id: string, params: any) {
|
|
955
949
|
const cfg = getConfig(api);
|
|
956
950
|
const supabase = getSupabase(cfg);
|
|
957
|
-
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
|
|
951
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id, api);
|
|
958
952
|
|
|
959
953
|
// Profile gate
|
|
960
954
|
const { data: profile } = await supabase.rpc("get_profile", { p_device_id: deviceId });
|
|
@@ -973,7 +967,7 @@ export default function register(api: any) {
|
|
|
973
967
|
} catch {}
|
|
974
968
|
}
|
|
975
969
|
|
|
976
|
-
const { data, error } = await supabase.rpc("join_event", { p_code: params.code, p_device_id: deviceId, p_lat: lat
|
|
970
|
+
const { data, error } = await supabase.rpc("join_event", { p_code: params.code, p_device_id: deviceId, p_lat: lat || null, p_lng: lng || null, p_application_context: params.application_context || null });
|
|
977
971
|
if (error) return ok({ error: error.message });
|
|
978
972
|
if (!data?.joined) return ok(data);
|
|
979
973
|
|
|
@@ -1036,7 +1030,7 @@ export default function register(api: any) {
|
|
|
1036
1030
|
async execute(_id: string, params: any) {
|
|
1037
1031
|
const cfg = getConfig(api);
|
|
1038
1032
|
const supabase = getSupabase(cfg);
|
|
1039
|
-
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
|
|
1033
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id, api);
|
|
1040
1034
|
|
|
1041
1035
|
const { data, error } = await supabase.rpc("event_participants_list", { p_code: params.code, p_device_id: deviceId });
|
|
1042
1036
|
if (error) return ok({ error: error.message });
|
|
@@ -1046,7 +1040,7 @@ export default function register(api: any) {
|
|
|
1046
1040
|
const profiles = others.map((p, i) => {
|
|
1047
1041
|
const ref = String(i + 1);
|
|
1048
1042
|
_refMap[ref] = p.device_id;
|
|
1049
|
-
return { ref,
|
|
1043
|
+
return { ref, name: p.display_name || "匿名", personal_description: p.line1, looking_for: p.line2, conversation_style: p.line3, profile_slug: p.profile_slug || null, checked_in: !!p.checked_in, role: p.role || "participant", status: p.status || "active", application_context: p.application_context || null, source: "event" };
|
|
1050
1044
|
});
|
|
1051
1045
|
|
|
1052
1046
|
(api as any)._antennaRefMap = { ...(api as any)._antennaRefMap, ..._refMap };
|
|
@@ -1076,7 +1070,7 @@ export default function register(api: any) {
|
|
|
1076
1070
|
async execute(_id: string, params: any) {
|
|
1077
1071
|
const cfg = getConfig(api);
|
|
1078
1072
|
const supabase = getSupabase(cfg);
|
|
1079
|
-
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
|
|
1073
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id, api);
|
|
1080
1074
|
|
|
1081
1075
|
let targetId = params.target_device_id;
|
|
1082
1076
|
if (!targetId && params.ref) {
|
|
@@ -1113,7 +1107,7 @@ export default function register(api: any) {
|
|
|
1113
1107
|
async execute(_id: string, params: any) {
|
|
1114
1108
|
const cfg = getConfig(api);
|
|
1115
1109
|
const supabase = getSupabase(cfg);
|
|
1116
|
-
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
|
|
1110
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id, api);
|
|
1117
1111
|
const fuzzy = (params.lat != null && params.lng != null) ? fuzzyCoords(params.lat, params.lng) : { lat: null, lng: null };
|
|
1118
1112
|
const { data, error } = await supabase.rpc("event_checkin", {
|
|
1119
1113
|
p_code: params.code,
|
|
@@ -1172,7 +1166,7 @@ export default function register(api: any) {
|
|
|
1172
1166
|
async execute(_id: string, params: any) {
|
|
1173
1167
|
const cfg = getConfig(api);
|
|
1174
1168
|
const supabase = getSupabase(cfg);
|
|
1175
|
-
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
|
|
1169
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id, api);
|
|
1176
1170
|
|
|
1177
1171
|
const { data: result } = await supabase.rpc("get_my_matches_with_profiles", { p_device_id: deviceId });
|
|
1178
1172
|
|
|
@@ -1187,18 +1181,18 @@ export default function register(api: any) {
|
|
|
1187
1181
|
ref: String(i + 1),
|
|
1188
1182
|
_device_id: m.target_id,
|
|
1189
1183
|
name: m.name || "匿名",
|
|
1190
|
-
|
|
1191
|
-
|
|
1184
|
+
personal_description: m.line1, looking_for: m.line2, conversation_style: m.line3,
|
|
1185
|
+
profile_slug: m.profile_slug || null,
|
|
1192
1186
|
their_contact: m.their_contact || null,
|
|
1193
1187
|
you_shared: m.you_shared || null,
|
|
1194
1188
|
}));
|
|
1195
1189
|
|
|
1196
1190
|
const incomingAccepts = rawIncoming.map((m: any, i: number) => ({
|
|
1197
|
-
ref: String(
|
|
1191
|
+
ref: String(i + 1),
|
|
1198
1192
|
_device_id: m.target_id,
|
|
1199
1193
|
name: m.name || "匿名",
|
|
1200
|
-
|
|
1201
|
-
|
|
1194
|
+
personal_description: m.line1, looking_for: m.line2, conversation_style: m.line3,
|
|
1195
|
+
profile_slug: m.profile_slug || null,
|
|
1202
1196
|
}));
|
|
1203
1197
|
|
|
1204
1198
|
// Clean up follow-up crons for mutual matches
|
|
@@ -1212,14 +1206,6 @@ export default function register(api: any) {
|
|
|
1212
1206
|
if (incomingAccepts.length > 0) messages.push(`${incomingAccepts.length} 个人想认识你,等你回应`);
|
|
1213
1207
|
if (messages.length === 0) messages.push("你接受了一些匹配,但对方还没有回应。耐心等等 ⏳");
|
|
1214
1208
|
|
|
1215
|
-
// Persist ref map so accept(ref) resolves correctly
|
|
1216
|
-
const _refMap: Record<string, string> = {};
|
|
1217
|
-
for (const m of mutualMatches) _refMap[m.ref] = m._device_id;
|
|
1218
|
-
for (const m of incomingAccepts) _refMap[m.ref] = m._device_id;
|
|
1219
|
-
if (deviceId && Object.keys(_refMap).length > 0) {
|
|
1220
|
-
try { await supabase.rpc("save_scan_refs", { p_owner: deviceId, p_refs: _refMap }); } catch { /* best effort */ }
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
1209
|
return ok({
|
|
1224
1210
|
mutual_matches: mutualMatches,
|
|
1225
1211
|
incoming_accepts: incomingAccepts,
|
|
@@ -1248,22 +1234,18 @@ export default function register(api: any) {
|
|
|
1248
1234
|
lng: { type: "number", description: "New event longitude" },
|
|
1249
1235
|
starts_at: { type: "string", description: "New start time ISO" },
|
|
1250
1236
|
ends_at: { type: "string", description: "New end time ISO" },
|
|
1251
|
-
requires_approval: { type: "boolean", description: "Require host approval to join" },
|
|
1252
|
-
screening_questions: { type: "array", items: { type: "string" }, description: "Screening questions for applicants" },
|
|
1253
1237
|
},
|
|
1254
1238
|
required: ["code", "sender_id", "channel", "chat_id"],
|
|
1255
1239
|
},
|
|
1256
1240
|
async execute(_id: string, params: any) {
|
|
1257
1241
|
const cfg = getConfig(api);
|
|
1258
1242
|
const supabase = getSupabase(cfg);
|
|
1259
|
-
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
|
|
1243
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id, api);
|
|
1260
1244
|
const { data, error } = await supabase.rpc("update_event", {
|
|
1261
1245
|
p_code: params.code, p_device_id: deviceId,
|
|
1262
1246
|
p_name: params.name || null, p_description: params.description || null,
|
|
1263
|
-
p_og_image: params.og_image || null, p_lat: params.lat
|
|
1247
|
+
p_og_image: params.og_image || null, p_lat: params.lat || null, p_lng: params.lng || null,
|
|
1264
1248
|
p_starts_at: params.starts_at || null, p_ends_at: params.ends_at || null,
|
|
1265
|
-
...(params.requires_approval != null ? { p_requires_approval: params.requires_approval } : {}),
|
|
1266
|
-
...(params.screening_questions != null ? { p_screening_questions: params.screening_questions } : {}),
|
|
1267
1249
|
});
|
|
1268
1250
|
if (error) return ok({ error: error.message });
|
|
1269
1251
|
return ok(data);
|
|
@@ -1290,7 +1272,7 @@ export default function register(api: any) {
|
|
|
1290
1272
|
async execute(_id: string, params: any) {
|
|
1291
1273
|
const cfg = getConfig(api);
|
|
1292
1274
|
const supabase = getSupabase(cfg);
|
|
1293
|
-
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
|
|
1275
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id, api);
|
|
1294
1276
|
const { data, error } = await supabase.rpc("approve_participant", {
|
|
1295
1277
|
p_code: params.code, p_device_id: deviceId, p_target_ref: params.ref,
|
|
1296
1278
|
});
|
|
@@ -1319,7 +1301,7 @@ export default function register(api: any) {
|
|
|
1319
1301
|
async execute(_id: string, params: any) {
|
|
1320
1302
|
const cfg = getConfig(api);
|
|
1321
1303
|
const supabase = getSupabase(cfg);
|
|
1322
|
-
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
|
|
1304
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id, api);
|
|
1323
1305
|
const { data, error } = await supabase.rpc("reject_participant", {
|
|
1324
1306
|
p_code: params.code, p_device_id: deviceId, p_target_ref: params.ref,
|
|
1325
1307
|
});
|
|
@@ -1348,7 +1330,7 @@ export default function register(api: any) {
|
|
|
1348
1330
|
async execute(_id: string, params: any) {
|
|
1349
1331
|
const cfg = getConfig(api);
|
|
1350
1332
|
const supabase = getSupabase(cfg);
|
|
1351
|
-
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
|
|
1333
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id, api);
|
|
1352
1334
|
const { data, error } = await supabase.rpc("add_cohost", {
|
|
1353
1335
|
p_code: params.code, p_device_id: deviceId, p_target_ref: params.ref,
|
|
1354
1336
|
});
|
|
@@ -1362,34 +1344,72 @@ export default function register(api: any) {
|
|
|
1362
1344
|
// ═══════════════════════════════════════════════════════════════════
|
|
1363
1345
|
api.registerTool({
|
|
1364
1346
|
name: "antenna_event_message",
|
|
1365
|
-
description:
|
|
1347
|
+
description:
|
|
1348
|
+
"Send a message to event participants. Only creator or co-host can send. Omit ref to broadcast to all.",
|
|
1366
1349
|
parameters: {
|
|
1367
1350
|
type: "object",
|
|
1368
1351
|
properties: {
|
|
1369
1352
|
code: { type: "string", description: "Event code" },
|
|
1370
|
-
sender_id: { type: "string" },
|
|
1371
|
-
channel: { type: "string" },
|
|
1353
|
+
sender_id: { type: "string", description: "The sender's user ID" },
|
|
1354
|
+
channel: { type: "string", description: "Channel name" },
|
|
1372
1355
|
chat_id: { type: "string", description: "REQUIRED for notifications. Pass the chat/channel ID from your message context so Antenna can send you match and event notifications." },
|
|
1373
|
-
message: { type: "string", description: "Message to send
|
|
1374
|
-
ref: { type: "string", description: "Ref number of specific participant (omit
|
|
1356
|
+
message: { type: "string", description: "Message to send" },
|
|
1357
|
+
ref: { type: "string", description: "Ref number of specific participant (omit for broadcast)" },
|
|
1375
1358
|
},
|
|
1376
|
-
required: ["code", "sender_id", "channel", "
|
|
1359
|
+
required: ["code", "sender_id", "channel", "chat_id", "message"],
|
|
1377
1360
|
},
|
|
1378
1361
|
async execute(_id: string, params: any) {
|
|
1379
1362
|
const cfg = getConfig(api);
|
|
1380
1363
|
const supabase = getSupabase(cfg);
|
|
1381
|
-
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
|
|
1382
|
-
const
|
|
1364
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id, api);
|
|
1365
|
+
const rpcParams: any = {
|
|
1383
1366
|
p_code: params.code,
|
|
1384
1367
|
p_device_id: deviceId,
|
|
1385
1368
|
p_message: params.message,
|
|
1386
|
-
|
|
1387
|
-
|
|
1369
|
+
};
|
|
1370
|
+
if (params.ref) rpcParams.p_target_ref = params.ref;
|
|
1371
|
+
const { data, error } = await supabase.rpc("send_event_message", rpcParams);
|
|
1388
1372
|
if (error) return ok({ error: error.message });
|
|
1389
1373
|
return ok(data);
|
|
1390
1374
|
},
|
|
1391
1375
|
});
|
|
1392
1376
|
|
|
1377
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1378
|
+
// Tool: antenna_link_account
|
|
1379
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1380
|
+
api.registerTool({
|
|
1381
|
+
name: "antenna_link_account",
|
|
1382
|
+
description:
|
|
1383
|
+
"Link your Antenna agent profile to your antenna.fyi website account. Pass the user's API key — the server verifies it and extracts the user_id. The agent never needs to know or pass user_id directly.",
|
|
1384
|
+
parameters: {
|
|
1385
|
+
type: "object",
|
|
1386
|
+
properties: {
|
|
1387
|
+
sender_id: { type: "string", description: "The sender's user ID" },
|
|
1388
|
+
channel: { type: "string", description: "Channel name" },
|
|
1389
|
+
chat_id: { type: "string", description: "REQUIRED for notifications. Pass the chat/channel ID from your message context so Antenna can send you match and event notifications." },
|
|
1390
|
+
api_key: { type: "string", description: "The user's Antenna API key (ant_xxx) from antenna.fyi/me" },
|
|
1391
|
+
},
|
|
1392
|
+
required: ["sender_id", "channel", "chat_id", "api_key"],
|
|
1393
|
+
},
|
|
1394
|
+
async execute(_id: string, params: any) {
|
|
1395
|
+
const cfg = getConfig(api);
|
|
1396
|
+
const supabase = getSupabase(cfg);
|
|
1397
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id, api);
|
|
1398
|
+
const { data, error } = await supabase.rpc("bind_user_id", {
|
|
1399
|
+
p_device_id: deviceId,
|
|
1400
|
+
p_api_key: params.api_key,
|
|
1401
|
+
});
|
|
1402
|
+
if (error) return ok({ error: error.message });
|
|
1403
|
+
if (data?.error) return ok(data);
|
|
1404
|
+
return ok({
|
|
1405
|
+
...data,
|
|
1406
|
+
message: "账号已关联!现在你可以在 antenna.fyi/me 看到你的完整 profile 和匹配记录了。",
|
|
1407
|
+
});
|
|
1408
|
+
},
|
|
1409
|
+
});
|
|
1410
|
+
|
|
1411
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1412
|
+
// Service: poll for new matches every 10 minutes → notify instantly
|
|
1393
1413
|
// ═══════════════════════════════════════════════════════════════════
|
|
1394
1414
|
const _notifiedMatches = new Set<string>(); // "deviceA→deviceB" already notified
|
|
1395
1415
|
|
|
@@ -1412,10 +1432,11 @@ export default function register(api: any) {
|
|
|
1412
1432
|
async (payload: any) => {
|
|
1413
1433
|
try {
|
|
1414
1434
|
const targetDeviceId = payload.new?.device_id_b;
|
|
1415
|
-
if (!targetDeviceId) return;
|
|
1435
|
+
if (!targetDeviceId || !_knownDeviceIds.has(targetDeviceId)) return;
|
|
1416
1436
|
|
|
1417
1437
|
const key = `${payload.new.device_id_a}→${targetDeviceId}`;
|
|
1418
1438
|
if (_notifiedMatches.has(key)) return;
|
|
1439
|
+
_notifiedMatches.add(key);
|
|
1419
1440
|
|
|
1420
1441
|
const parts = targetDeviceId.split(":");
|
|
1421
1442
|
if (parts.length < 2) return;
|
|
@@ -1427,7 +1448,6 @@ export default function register(api: any) {
|
|
|
1427
1448
|
|
|
1428
1449
|
const { data: theirProfile } = await innerSb.rpc("get_profile", { p_device_id: payload.new.device_id_a });
|
|
1429
1450
|
const name = theirProfile?.display_name || "有人";
|
|
1430
|
-
const emoji = theirProfile?.emoji || "👤";
|
|
1431
1451
|
|
|
1432
1452
|
// Check if mutual
|
|
1433
1453
|
const { data: matches } = await innerSb.rpc("get_my_matches", { p_device_id: targetDeviceId });
|
|
@@ -1438,15 +1458,13 @@ export default function register(api: any) {
|
|
|
1438
1458
|
if (myAccept) {
|
|
1439
1459
|
const contact = payload.new.contact_info_a ? `\n对方的联系方式:${payload.new.contact_info_a}` : "";
|
|
1440
1460
|
notifyUser(channel, userId,
|
|
1441
|
-
`[Antenna] 🎉 双向匹配!${
|
|
1442
|
-
logger);
|
|
1443
|
-
_notifiedMatches.add(key);
|
|
1461
|
+
`[Antenna] 🎉 双向匹配!${name} 也接受了你!${contact}\n\n用 antenna_check_matches 查看详情。`,
|
|
1462
|
+
logger, api);
|
|
1444
1463
|
stopFollowUpCron(targetDeviceId, payload.new.device_id_a, logger);
|
|
1445
1464
|
} else {
|
|
1446
1465
|
notifyUser(channel, userId,
|
|
1447
|
-
`[Antenna] 📩 ${
|
|
1448
|
-
logger);
|
|
1449
|
-
_notifiedMatches.add(key);
|
|
1466
|
+
`[Antenna] 📩 ${name} 想认识你!看看 TA 的名片,决定要不要接受?\n\n用 antenna_check_matches 查看详情。`,
|
|
1467
|
+
logger, api);
|
|
1450
1468
|
}
|
|
1451
1469
|
} catch (err: any) {
|
|
1452
1470
|
logger.warn("Antenna: realtime match handler error:", err.message);
|
|
@@ -1483,13 +1501,12 @@ export default function register(api: any) {
|
|
|
1483
1501
|
// Get applicant profile
|
|
1484
1502
|
const { data: applicant } = await epSb.rpc('get_profile', { p_device_id: applicantDeviceId });
|
|
1485
1503
|
const aName = applicant?.display_name || '某人';
|
|
1486
|
-
const aEmoji = applicant?.emoji || '👤';
|
|
1487
1504
|
|
|
1488
1505
|
const parts = event.created_by.split(':');
|
|
1489
1506
|
if (parts.length < 2) return;
|
|
1490
1507
|
notifyUser(parts[0], parts.slice(1).join(':'),
|
|
1491
|
-
`[Antenna] 📩 ${
|
|
1492
|
-
logger);
|
|
1508
|
+
`[Antenna] 📩 ${aName} 申请加入你的活动「${event.name}」\n\n用 antenna_event_scan --code ${event.code} 查看申请者名片并审批。`,
|
|
1509
|
+
logger, api);
|
|
1493
1510
|
} catch (err: any) {
|
|
1494
1511
|
logger.warn('Antenna: event participant INSERT handler error:', err.message);
|
|
1495
1512
|
}
|
|
@@ -1518,11 +1535,11 @@ export default function register(api: any) {
|
|
|
1518
1535
|
if (newStatus === 'active') {
|
|
1519
1536
|
notifyUser(parts[0], parts.slice(1).join(':'),
|
|
1520
1537
|
`[Antenna] ✅ 你的申请已通过!欢迎加入「${eventName}」\n\n用 antenna_event_scan --code ${event?.code} 查看其他参与者。`,
|
|
1521
|
-
logger);
|
|
1538
|
+
logger, api);
|
|
1522
1539
|
} else if (newStatus === 'rejected') {
|
|
1523
1540
|
notifyUser(parts[0], parts.slice(1).join(':'),
|
|
1524
1541
|
`[Antenna] ❌ 你的申请未通过「${eventName}」的审核。`,
|
|
1525
|
-
logger);
|
|
1542
|
+
logger, api);
|
|
1526
1543
|
}
|
|
1527
1544
|
} catch (err: any) {
|
|
1528
1545
|
logger.warn('Antenna: event participant UPDATE handler error:', err.message);
|
|
@@ -1542,9 +1559,10 @@ export default function register(api: any) {
|
|
|
1542
1559
|
const cfg = getConfig(api);
|
|
1543
1560
|
const supabase = getSupabase(cfg);
|
|
1544
1561
|
|
|
1545
|
-
// Get all profiles
|
|
1562
|
+
// Get all profiles that have been active in last 24h
|
|
1546
1563
|
const { data: activeProfiles } = await supabase
|
|
1547
|
-
.rpc("
|
|
1564
|
+
.rpc("nearby_profiles", { p_lat: 0, p_lng: 0, p_radius_m: 999999999 })
|
|
1565
|
+
.select("device_id");
|
|
1548
1566
|
|
|
1549
1567
|
if (!activeProfiles?.length) return;
|
|
1550
1568
|
|
|
@@ -1574,7 +1592,7 @@ export default function register(api: any) {
|
|
|
1574
1592
|
|
|
1575
1593
|
for (const match of newMatches) {
|
|
1576
1594
|
const notifyKey = `${match.device_id_a}→${match.device_id_b}`;
|
|
1577
|
-
|
|
1595
|
+
_notifiedMatches.add(notifyKey);
|
|
1578
1596
|
|
|
1579
1597
|
// Is this a new mutual match?
|
|
1580
1598
|
if (match.device_id_a === deviceId) {
|
|
@@ -1582,38 +1600,36 @@ export default function register(api: any) {
|
|
|
1582
1600
|
if (reverse) {
|
|
1583
1601
|
const { data: theirProfile } = await supabase.rpc("get_profile", { p_device_id: match.device_id_b });
|
|
1584
1602
|
const name = theirProfile?.display_name || "对方";
|
|
1585
|
-
const emoji = theirProfile?.emoji || "👤";
|
|
1586
1603
|
const contact = reverse.contact_info_a ? `\n对方的联系方式:${reverse.contact_info_a}` : "";
|
|
1587
1604
|
notifyUser(
|
|
1588
1605
|
channel, userId,
|
|
1589
|
-
`[Antenna] 🎉 双向匹配成功!${
|
|
1590
|
-
logger,
|
|
1606
|
+
`[Antenna] 🎉 双向匹配成功!${name} 也接受了你!${contact}\n\n用 antenna_check_matches 查看详情。`,
|
|
1607
|
+
logger, api,
|
|
1591
1608
|
);
|
|
1592
|
-
|
|
1609
|
+
// Clean up follow-up crons
|
|
1593
1610
|
stopFollowUpCron(deviceId, match.device_id_b, logger);
|
|
1594
1611
|
}
|
|
1595
1612
|
} else if (match.device_id_b === deviceId) {
|
|
1596
1613
|
// Someone new accepted me
|
|
1597
1614
|
const { data: theirProfile } = await supabase.rpc("get_profile", { p_device_id: match.device_id_a });
|
|
1598
1615
|
const name = theirProfile?.display_name || "有人";
|
|
1599
|
-
const emoji = theirProfile?.emoji || "👤";
|
|
1600
1616
|
const iAccepted = myMatches.find((m: any) => m.device_id_b === match.device_id_a);
|
|
1601
1617
|
if (iAccepted) {
|
|
1618
|
+
// I already accepted them → mutual!
|
|
1602
1619
|
const contact = match.contact_info_a ? `\n对方的联系方式:${match.contact_info_a}` : "";
|
|
1603
1620
|
notifyUser(
|
|
1604
1621
|
channel, userId,
|
|
1605
|
-
`[Antenna] 🎉 双向匹配成功!${
|
|
1606
|
-
logger,
|
|
1622
|
+
`[Antenna] 🎉 双向匹配成功!${name} 也接受了你!${contact}\n\n用 antenna_check_matches 查看详情。`,
|
|
1623
|
+
logger, api,
|
|
1607
1624
|
);
|
|
1608
|
-
_notifiedMatches.add(notifyKey);
|
|
1609
1625
|
stopFollowUpCron(deviceId, match.device_id_a, logger);
|
|
1610
1626
|
} else {
|
|
1627
|
+
// They accepted me but I haven't responded
|
|
1611
1628
|
notifyUser(
|
|
1612
1629
|
channel, userId,
|
|
1613
|
-
`[Antenna] 📩 ${
|
|
1614
|
-
logger,
|
|
1630
|
+
`[Antenna] 📩 ${name} 想认识你!看看 TA 的名片,决定要不要接受?\n\n用 antenna_check_matches 查看详情。`,
|
|
1631
|
+
logger, api,
|
|
1615
1632
|
);
|
|
1616
|
-
_notifiedMatches.add(notifyKey);
|
|
1617
1633
|
}
|
|
1618
1634
|
}
|
|
1619
1635
|
}
|
|
@@ -1624,9 +1640,8 @@ export default function register(api: any) {
|
|
|
1624
1640
|
}
|
|
1625
1641
|
}
|
|
1626
1642
|
|
|
1627
|
-
// ── Event approval polling
|
|
1628
|
-
for (const
|
|
1629
|
-
const deviceId = profile.device_id;
|
|
1643
|
+
// ── Event approval polling ──
|
|
1644
|
+
for (const deviceId of _knownDeviceIds) {
|
|
1630
1645
|
try {
|
|
1631
1646
|
const { data: events } = await supabase.rpc("get_my_event_updates", { p_device_id: deviceId });
|
|
1632
1647
|
if (!events?.length) continue;
|
|
@@ -1635,47 +1650,23 @@ export default function register(api: any) {
|
|
|
1635
1650
|
const channel = parts[0];
|
|
1636
1651
|
const userId = parts.slice(1).join(":");
|
|
1637
1652
|
for (const ev of events) {
|
|
1638
|
-
const key = `event:${
|
|
1653
|
+
const key = `event:${ev.event_id}:${ev.status}`;
|
|
1639
1654
|
if (_notifiedMatches.has(key)) continue;
|
|
1640
|
-
|
|
1655
|
+
_notifiedMatches.add(key);
|
|
1656
|
+
if (ev.status === "active" && ev.role !== "creator" && ev.role !== "cohost") {
|
|
1641
1657
|
notifyUser(channel, userId,
|
|
1642
1658
|
`[Antenna] ✅ 你的申请已通过!欢迎加入「${ev.event_name}」`,
|
|
1643
|
-
logger,
|
|
1659
|
+
logger, api,
|
|
1644
1660
|
);
|
|
1645
|
-
_notifiedMatches.add(key);
|
|
1646
1661
|
} else if (ev.status === "rejected") {
|
|
1647
1662
|
notifyUser(channel, userId,
|
|
1648
1663
|
`[Antenna] ❌ 你的申请未通过「${ev.event_name}」`,
|
|
1649
|
-
logger,
|
|
1664
|
+
logger, api,
|
|
1650
1665
|
);
|
|
1651
|
-
_notifiedMatches.add(key);
|
|
1652
1666
|
}
|
|
1653
1667
|
}
|
|
1654
1668
|
} catch { /* silent */ }
|
|
1655
1669
|
}
|
|
1656
|
-
|
|
1657
|
-
// ── Event messages polling ──
|
|
1658
|
-
for (const profile of activeProfiles) {
|
|
1659
|
-
const deviceId = profile.device_id;
|
|
1660
|
-
try {
|
|
1661
|
-
const { data: msgs } = await supabase.rpc("get_my_event_messages", { p_device_id: deviceId });
|
|
1662
|
-
if (!msgs?.length) continue;
|
|
1663
|
-
const parts = deviceId.split(":");
|
|
1664
|
-
if (parts.length < 2) continue;
|
|
1665
|
-
const channel = parts[0];
|
|
1666
|
-
const userId = parts.slice(1).join(":");
|
|
1667
|
-
for (const msg of msgs) {
|
|
1668
|
-
const key = `evtmsg:${msg.event_id}:${msg.created_at}`;
|
|
1669
|
-
if (_notifiedMatches.has(key)) continue;
|
|
1670
|
-
const role = msg.sender_role === 'creator' ? '组织者' : '协办';
|
|
1671
|
-
notifyUser(channel, userId,
|
|
1672
|
-
`[Antenna] 📢 来自「${msg.event_name}」${role} ${msg.sender_emoji || ''} ${msg.sender_name}: ${msg.message}`,
|
|
1673
|
-
logger,
|
|
1674
|
-
);
|
|
1675
|
-
_notifiedMatches.add(key);
|
|
1676
|
-
}
|
|
1677
|
-
} catch { /* silent */ }
|
|
1678
|
-
}
|
|
1679
1670
|
} catch (err: any) {
|
|
1680
1671
|
logger.warn("Antenna: match poll error:", err.message);
|
|
1681
1672
|
}
|
|
@@ -1695,6 +1686,171 @@ export default function register(api: any) {
|
|
|
1695
1686
|
},
|
|
1696
1687
|
});
|
|
1697
1688
|
|
|
1689
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1690
|
+
// Tool: antenna_drift_throw
|
|
1691
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1692
|
+
api.registerTool({
|
|
1693
|
+
name: "antenna_drift_throw",
|
|
1694
|
+
description: "Throw a drift bottle into the sea. Write a message (max 500 chars), a random stranger will pick it up. Anonymous.",
|
|
1695
|
+
parameters: {
|
|
1696
|
+
type: "object",
|
|
1697
|
+
properties: {
|
|
1698
|
+
sender_id: { type: "string", description: "The sender's user ID" },
|
|
1699
|
+
channel: { type: "string", description: "The channel name" },
|
|
1700
|
+
chat_id: { type: "string", description: "REQUIRED. Chat/channel ID for notifications." },
|
|
1701
|
+
message: { type: "string", description: "The message to put in the bottle (max 500 chars)" },
|
|
1702
|
+
},
|
|
1703
|
+
required: ["sender_id", "channel", "chat_id", "message"],
|
|
1704
|
+
},
|
|
1705
|
+
async execute(_id: string, params: any) {
|
|
1706
|
+
const cfg = getConfig(api);
|
|
1707
|
+
const supabase = getSupabase(cfg);
|
|
1708
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id, api);
|
|
1709
|
+
|
|
1710
|
+
if (!params.message || params.message.length === 0) {
|
|
1711
|
+
return ok({ error: "empty_message", message: "漂流瓶不能是空的。" });
|
|
1712
|
+
}
|
|
1713
|
+
if (params.message.length > 500) {
|
|
1714
|
+
return ok({ error: "too_long", message: "最多 500 字。" });
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
const { data, error } = await supabase.rpc("throw_drift_bottle", {
|
|
1718
|
+
p_device_id: deviceId,
|
|
1719
|
+
p_message: params.message,
|
|
1720
|
+
});
|
|
1721
|
+
|
|
1722
|
+
if (error) return ok({ error: error.message });
|
|
1723
|
+
return ok({ success: true, bottle_id: data?.bottle_id, total_thrown: data?.total_thrown, message: "🍾 漂流瓶已丢入海中,等待某个陌生人捡起它…" });
|
|
1724
|
+
},
|
|
1725
|
+
});
|
|
1726
|
+
|
|
1727
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1728
|
+
// Tool: antenna_drift_pick
|
|
1729
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1730
|
+
api.registerTool({
|
|
1731
|
+
name: "antenna_drift_pick",
|
|
1732
|
+
description: "Pick up a random drift bottle from the sea. You'll get an anonymous message from a stranger. You can only hold one bottle at a time.",
|
|
1733
|
+
parameters: {
|
|
1734
|
+
type: "object",
|
|
1735
|
+
properties: {
|
|
1736
|
+
sender_id: { type: "string", description: "The sender's user ID" },
|
|
1737
|
+
channel: { type: "string", description: "The channel name" },
|
|
1738
|
+
chat_id: { type: "string", description: "REQUIRED. Chat/channel ID for notifications." },
|
|
1739
|
+
},
|
|
1740
|
+
required: ["sender_id", "channel", "chat_id"],
|
|
1741
|
+
},
|
|
1742
|
+
async execute(_id: string, params: any) {
|
|
1743
|
+
const cfg = getConfig(api);
|
|
1744
|
+
const supabase = getSupabase(cfg);
|
|
1745
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id, api);
|
|
1746
|
+
|
|
1747
|
+
const { data, error } = await supabase.rpc("pick_drift_bottle", {
|
|
1748
|
+
p_device_id: deviceId,
|
|
1749
|
+
});
|
|
1750
|
+
|
|
1751
|
+
if (error) return ok({ error: error.message });
|
|
1752
|
+
return ok(data);
|
|
1753
|
+
},
|
|
1754
|
+
});
|
|
1755
|
+
|
|
1756
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1757
|
+
// Tool: antenna_drift_reply
|
|
1758
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1759
|
+
api.registerTool({
|
|
1760
|
+
name: "antenna_drift_reply",
|
|
1761
|
+
description: "Reply to a drift bottle you picked up. Your reply will be sent back to the original thrower anonymously.",
|
|
1762
|
+
parameters: {
|
|
1763
|
+
type: "object",
|
|
1764
|
+
properties: {
|
|
1765
|
+
sender_id: { type: "string", description: "The sender's user ID" },
|
|
1766
|
+
channel: { type: "string", description: "The channel name" },
|
|
1767
|
+
chat_id: { type: "string", description: "REQUIRED. Chat/channel ID for notifications." },
|
|
1768
|
+
bottle_id: { type: "string", description: "The bottle ID to reply to" },
|
|
1769
|
+
reply: { type: "string", description: "Your reply message (max 500 chars)" },
|
|
1770
|
+
},
|
|
1771
|
+
required: ["sender_id", "channel", "chat_id", "bottle_id", "reply"],
|
|
1772
|
+
},
|
|
1773
|
+
async execute(_id: string, params: any) {
|
|
1774
|
+
const cfg = getConfig(api);
|
|
1775
|
+
const supabase = getSupabase(cfg);
|
|
1776
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id, api);
|
|
1777
|
+
|
|
1778
|
+
if (!params.reply || params.reply.length === 0) {
|
|
1779
|
+
return ok({ error: "empty_reply", message: "回复不能是空的。" });
|
|
1780
|
+
}
|
|
1781
|
+
if (params.reply.length > 500) {
|
|
1782
|
+
return ok({ error: "too_long", message: "最多 500 字。" });
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
const { data, error } = await supabase.rpc("reply_drift_bottle", {
|
|
1786
|
+
p_bottle_id: params.bottle_id,
|
|
1787
|
+
p_device_id: deviceId,
|
|
1788
|
+
p_reply: params.reply,
|
|
1789
|
+
});
|
|
1790
|
+
|
|
1791
|
+
if (error) return ok({ error: error.message });
|
|
1792
|
+
return ok(data);
|
|
1793
|
+
},
|
|
1794
|
+
});
|
|
1795
|
+
|
|
1796
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1797
|
+
// Tool: antenna_drift_check
|
|
1798
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1799
|
+
api.registerTool({
|
|
1800
|
+
name: "antenna_drift_check",
|
|
1801
|
+
description: "Check drift bottle status: any new replies to your bottles? Any bottles you picked up waiting for reply?",
|
|
1802
|
+
parameters: {
|
|
1803
|
+
type: "object",
|
|
1804
|
+
properties: {
|
|
1805
|
+
sender_id: { type: "string", description: "The sender's user ID" },
|
|
1806
|
+
channel: { type: "string", description: "The channel name" },
|
|
1807
|
+
chat_id: { type: "string", description: "REQUIRED. Chat/channel ID for notifications." },
|
|
1808
|
+
},
|
|
1809
|
+
required: ["sender_id", "channel", "chat_id"],
|
|
1810
|
+
},
|
|
1811
|
+
async execute(_id: string, params: any) {
|
|
1812
|
+
const cfg = getConfig(api);
|
|
1813
|
+
const supabase = getSupabase(cfg);
|
|
1814
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id, api);
|
|
1815
|
+
|
|
1816
|
+
const { data, error } = await supabase.rpc("check_drift_bottles", {
|
|
1817
|
+
p_device_id: deviceId,
|
|
1818
|
+
});
|
|
1819
|
+
|
|
1820
|
+
if (error) return ok({ error: error.message });
|
|
1821
|
+
return ok(data);
|
|
1822
|
+
},
|
|
1823
|
+
});
|
|
1824
|
+
|
|
1825
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1826
|
+
// Tool: antenna_drift_my_bottles
|
|
1827
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1828
|
+
api.registerTool({
|
|
1829
|
+
name: "antenna_drift_my_bottles",
|
|
1830
|
+
description: "List all drift bottles you've thrown, with their status (floating/picked/replied).",
|
|
1831
|
+
parameters: {
|
|
1832
|
+
type: "object",
|
|
1833
|
+
properties: {
|
|
1834
|
+
sender_id: { type: "string", description: "The sender's user ID" },
|
|
1835
|
+
channel: { type: "string", description: "The channel name" },
|
|
1836
|
+
chat_id: { type: "string", description: "REQUIRED. Chat/channel ID for notifications." },
|
|
1837
|
+
},
|
|
1838
|
+
required: ["sender_id", "channel", "chat_id"],
|
|
1839
|
+
},
|
|
1840
|
+
async execute(_id: string, params: any) {
|
|
1841
|
+
const cfg = getConfig(api);
|
|
1842
|
+
const supabase = getSupabase(cfg);
|
|
1843
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id, api);
|
|
1844
|
+
|
|
1845
|
+
const { data, error } = await supabase.rpc("get_my_bottles", {
|
|
1846
|
+
p_device_id: deviceId,
|
|
1847
|
+
});
|
|
1848
|
+
|
|
1849
|
+
if (error) return ok({ error: error.message });
|
|
1850
|
+
return ok(data);
|
|
1851
|
+
},
|
|
1852
|
+
});
|
|
1853
|
+
|
|
1698
1854
|
// ═══════════════════════════════════════════════════════════════════
|
|
1699
1855
|
// Hook: auto-scan when location is received
|
|
1700
1856
|
// ═══════════════════════════════════════════════════════════════════
|