antenna-openclaw-plugin 1.3.30 → 1.3.39
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 +390 -417
- package/openclaw.plugin.json +2 -2
- package/package.json +3 -3
- package/skills/antenna/EVENTS.md +163 -0
- package/skills/antenna/SKILL.md +237 -270
- package/migrations/drift_bottles.sql +0 -177
- package/tests/drift-bottle.test.ts +0 -109
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;
|
|
28
31
|
visible: boolean;
|
|
29
32
|
last_seen_at?: string;
|
|
30
|
-
profile_slug?: string;
|
|
31
|
-
distance_m?: number;
|
|
32
|
-
dist_meters?: number;
|
|
33
|
-
matching_context?: string;
|
|
34
33
|
}
|
|
35
34
|
|
|
36
35
|
interface MatchResult {
|
|
37
36
|
device_id: string;
|
|
38
37
|
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 ?? 168,
|
|
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
|
|
105
|
+
function deriveDeviceId(senderId: string, channel: string, chatId?: string): string {
|
|
106
106
|
const id = `${channel}:${senderId}`;
|
|
107
107
|
_knownDeviceIds.add(id);
|
|
108
|
-
if (chatId
|
|
108
|
+
if (chatId) {
|
|
109
109
|
_channelContext.set(id, chatId);
|
|
110
110
|
// Persist to DB async
|
|
111
111
|
try {
|
|
@@ -122,6 +122,32 @@ function ok(data: any) {
|
|
|
122
122
|
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
async function generateEmbeddingForQuery(cfg: AntennaConfig, text: string): Promise<number[] | null> {
|
|
126
|
+
try {
|
|
127
|
+
const supabaseUrl = cfg.supabaseUrl || BUILTIN_SUPABASE_URL;
|
|
128
|
+
const supabaseKey = cfg.supabaseKey || BUILTIN_SUPABASE_ANON_KEY;
|
|
129
|
+
const res = await fetch(`${supabaseUrl}/functions/v1/generate-embedding`, {
|
|
130
|
+
method: "POST",
|
|
131
|
+
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${supabaseKey}` },
|
|
132
|
+
body: JSON.stringify({ text }),
|
|
133
|
+
});
|
|
134
|
+
if (!res.ok) return null;
|
|
135
|
+
const data = await res.json();
|
|
136
|
+
return data?.embedding || null;
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function intentSearchReason(query: string, profile: any): string {
|
|
143
|
+
if (profile.recommendation_reason) return profile.recommendation_reason;
|
|
144
|
+
const tags = Array.isArray(profile.interest_tags) && profile.interest_tags.length
|
|
145
|
+
? ` Tags: ${profile.interest_tags.slice(0, 3).join(", ")}.`
|
|
146
|
+
: "";
|
|
147
|
+
const score = typeof profile.match_score === "number" ? ` Score: ${profile.match_score.toFixed(2)}.` : "";
|
|
148
|
+
return `Matches the intent "${query}".${tags}${score}`.trim();
|
|
149
|
+
}
|
|
150
|
+
|
|
125
151
|
// ─── Cron helpers ────────────────────────────────────────────────────
|
|
126
152
|
|
|
127
153
|
const FOLLOW_UP_INTERVAL_MS = 15 * 60 * 1000; // 15 minutes
|
|
@@ -140,13 +166,12 @@ async function notifyUser(
|
|
|
140
166
|
userId: string,
|
|
141
167
|
message: string,
|
|
142
168
|
logger: any,
|
|
143
|
-
api?: any,
|
|
144
169
|
): Promise<void> {
|
|
145
170
|
const deviceId = `${channel}:${userId}`;
|
|
146
171
|
let chatId = _channelContext.get(deviceId);
|
|
147
172
|
|
|
148
173
|
// Fallback: read from DB if not in memory
|
|
149
|
-
if (!chatId
|
|
174
|
+
if (!chatId) {
|
|
150
175
|
try {
|
|
151
176
|
const cfg = getConfig(api);
|
|
152
177
|
const sb = getSupabase(cfg);
|
|
@@ -276,7 +301,7 @@ export default function register(api: any) {
|
|
|
276
301
|
async execute(_id: string, params: any) {
|
|
277
302
|
const cfg = getConfig(api);
|
|
278
303
|
const supabase = getSupabase(cfg);
|
|
279
|
-
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id
|
|
304
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
|
|
280
305
|
const radius = params.radius_m ?? cfg.defaultRadiusM ?? 500;
|
|
281
306
|
|
|
282
307
|
if (isRateLimited(deviceId)) {
|
|
@@ -308,10 +333,38 @@ export default function register(api: any) {
|
|
|
308
333
|
const others = (nearby ?? []).filter((p: Profile) => p.device_id !== deviceId);
|
|
309
334
|
|
|
310
335
|
if (others.length === 0) {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
336
|
+
// Fallback to global discover
|
|
337
|
+
const { data: globalData } = await supabase.rpc("global_discover", {
|
|
338
|
+
p_device_id: deviceId, p_limit: 1,
|
|
314
339
|
});
|
|
340
|
+
const globalOthers = globalData || [];
|
|
341
|
+
if (globalOthers.length > 0) {
|
|
342
|
+
const gRefMap: Record<string, string> = {};
|
|
343
|
+
const gProfiles = globalOthers.map((p: any, i: number) => {
|
|
344
|
+
const ref = String(i + 1);
|
|
345
|
+
gRefMap[ref] = p.device_id;
|
|
346
|
+
return {
|
|
347
|
+
ref: ref,
|
|
348
|
+
name: p.display_name || "匿名",
|
|
349
|
+
personal_description: p.line1,
|
|
350
|
+
looking_for: p.line2,
|
|
351
|
+
conversation_style: p.line3,
|
|
352
|
+
more_information: p.matching_context || null,
|
|
353
|
+
profile_slug: p.profile_slug || null,
|
|
354
|
+
distance_m: p.distance_m ?? p.dist_meters ?? null,
|
|
355
|
+
};
|
|
356
|
+
});
|
|
357
|
+
(api as any)._antennaRefMap = gRefMap;
|
|
358
|
+
try { await supabase.rpc("save_scan_refs", { p_owner: deviceId, p_refs: gRefMap }); } catch {}
|
|
359
|
+
for (const p of globalOthers) {
|
|
360
|
+
try { await supabase.rpc("log_recommendation", { p_device_id: deviceId, p_recommended_id: p.device_id }); } catch {}
|
|
361
|
+
}
|
|
362
|
+
return ok({
|
|
363
|
+
profiles: gProfiles, count: gProfiles.length, radius_m: radius, global: true,
|
|
364
|
+
message: `附近 ${radius}m 暂时没人。今天的全球推荐——这个人跟你可能聊得来。(每天 1 次)`,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
return ok({ profiles: [], message: `附近暂时没人,今天的全球推荐已经用完了。明天再来!` });
|
|
315
368
|
}
|
|
316
369
|
|
|
317
370
|
// Build ref mapping — never expose device_id
|
|
@@ -320,11 +373,12 @@ export default function register(api: any) {
|
|
|
320
373
|
const ref = String(i + 1);
|
|
321
374
|
_refMap[ref] = p.device_id;
|
|
322
375
|
return {
|
|
323
|
-
ref,
|
|
376
|
+
ref: ref,
|
|
324
377
|
name: p.display_name || "匿名",
|
|
325
378
|
personal_description: p.line1,
|
|
326
379
|
looking_for: p.line2,
|
|
327
380
|
conversation_style: p.line3,
|
|
381
|
+
more_information: p.matching_context || null,
|
|
328
382
|
profile_slug: p.profile_slug || null,
|
|
329
383
|
distance_m: p.distance_m ?? p.dist_meters ?? null,
|
|
330
384
|
};
|
|
@@ -351,7 +405,7 @@ export default function register(api: any) {
|
|
|
351
405
|
api.registerTool({
|
|
352
406
|
name: "antenna_profile",
|
|
353
407
|
description:
|
|
354
|
-
"View or update the user's Antenna profile (name card). The profile has a display name and three
|
|
408
|
+
"View or update the user's Antenna profile (name card). The profile has a display name, emoji, and three lines describing who they are.",
|
|
355
409
|
parameters: {
|
|
356
410
|
type: "object",
|
|
357
411
|
properties: {
|
|
@@ -360,98 +414,45 @@ export default function register(api: any) {
|
|
|
360
414
|
channel: { type: "string", description: "The channel name" },
|
|
361
415
|
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." },
|
|
362
416
|
display_name: { type: "string", description: "Display name" },
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
417
|
+
emoji: { type: "string", description: "Profile emoji" },
|
|
418
|
+
line1: { type: "string", description: "First line (who you are / what you do)" },
|
|
419
|
+
line2: { type: "string", description: "Second line (what you're into)" },
|
|
420
|
+
line3: { type: "string", description: "Third line (what you're looking for)" },
|
|
366
421
|
visible: { type: "boolean", description: "Whether to be visible to others" },
|
|
367
|
-
|
|
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." },
|
|
422
|
+
matching_context: { type: "string", description: "More information / free-form context for AI matching (interests, goals, background, etc.)" },
|
|
373
423
|
},
|
|
374
424
|
required: ["action", "sender_id", "channel", "chat_id"],
|
|
375
425
|
},
|
|
376
426
|
async execute(_id: string, params: any) {
|
|
377
427
|
const cfg = getConfig(api);
|
|
378
428
|
const supabase = getSupabase(cfg);
|
|
379
|
-
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id
|
|
429
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
|
|
380
430
|
|
|
381
431
|
if (params.action === "get") {
|
|
382
432
|
const { data, error } = await supabase.rpc("get_profile", { p_device_id: deviceId });
|
|
383
433
|
if (error || !data) {
|
|
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
|
-
});
|
|
434
|
+
return ok({ exists: false, message: "你还没有名片。告诉我你的名字、一个 emoji、和三句话介绍自己,我帮你创建。" });
|
|
399
435
|
}
|
|
400
436
|
return ok({
|
|
401
437
|
exists: true,
|
|
402
|
-
profile: { display_name: data.display_name,
|
|
403
|
-
|
|
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
|
-
},
|
|
438
|
+
profile: { display_name: data.display_name, emoji: data.emoji,
|
|
439
|
+
line1: data.line1, line2: data.line2, line3: data.line3, visible: data.visible },
|
|
415
440
|
});
|
|
416
441
|
}
|
|
417
442
|
|
|
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
|
-
|
|
441
443
|
const { data, error } = await supabase.rpc("upsert_profile", {
|
|
442
444
|
p_device_id: deviceId,
|
|
443
|
-
p_display_name: params.display_name ?? null, p_emoji: null,
|
|
444
|
-
p_line1: params.
|
|
445
|
-
p_line3: params.
|
|
446
|
-
...(
|
|
447
|
-
...(params.contact_info != null ? { p_contact_info: params.contact_info } : {}),
|
|
445
|
+
p_display_name: params.display_name ?? null, p_emoji: params.emoji ?? null,
|
|
446
|
+
p_line1: params.line1 ?? null, p_line2: params.line2 ?? null,
|
|
447
|
+
p_line3: params.line3 ?? null, p_visible: params.visible ?? true,
|
|
448
|
+
...(params.matching_context != null ? { p_matching_context: params.matching_context } : {}),
|
|
448
449
|
});
|
|
449
450
|
|
|
450
451
|
if (error) return ok({ error: error.message });
|
|
451
452
|
|
|
452
453
|
// Read back profile to get slug for public page link
|
|
453
|
-
let publicUrl
|
|
454
|
-
let
|
|
454
|
+
let publicUrl = null;
|
|
455
|
+
let archetypeResult = null;
|
|
455
456
|
try {
|
|
456
457
|
const { data: profile } = await supabase.rpc("get_profile", { p_device_id: deviceId });
|
|
457
458
|
if (profile?.profile_slug) {
|
|
@@ -459,25 +460,50 @@ export default function register(api: any) {
|
|
|
459
460
|
}
|
|
460
461
|
} catch {}
|
|
461
462
|
|
|
462
|
-
//
|
|
463
|
+
// Generate personalized archetype description via LLM (best-effort)
|
|
463
464
|
try {
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
465
|
+
const profileText = [data.line1, data.line2, data.line3, params.matching_context].filter(Boolean).join(". ");
|
|
466
|
+
if (profileText) {
|
|
467
|
+
const corpus = profileText.toLowerCase();
|
|
468
|
+
const archetypeKw: Record<string, string[]> = {
|
|
469
|
+
Prometheus: ["ai", "agent", "llm", "founder", "startup", "build", "developer", "tools"],
|
|
470
|
+
Athena: ["product", "strategy", "research", "design", "craft", "pm", "ux"],
|
|
471
|
+
Hermes: ["network", "connect", "community", "social", "bridge"],
|
|
472
|
+
Apollo: ["music", "media", "content", "creator", "writing", "taste"],
|
|
473
|
+
Artemis: ["independent", "explore", "freelance", "health", "outdoor"],
|
|
474
|
+
Aphrodite: ["beauty", "brand", "fashion", "relationship"],
|
|
475
|
+
Dionysus: ["event", "culture", "party", "art", "festival"],
|
|
476
|
+
Hades: ["finance", "invest", "infrastructure", "backend", "security"],
|
|
477
|
+
Persephone: ["transform", "cross", "research", "academic", "bridge"],
|
|
478
|
+
Odysseus: ["founder", "journey", "resilience", "travel", "startup"],
|
|
479
|
+
};
|
|
480
|
+
let bestRole = "Prometheus"; let bestScore = 0;
|
|
481
|
+
for (const [role, kws] of Object.entries(archetypeKw)) {
|
|
482
|
+
const score = kws.reduce((s, kw) => s + (corpus.includes(kw) ? 1 : 0), 0);
|
|
483
|
+
if (score > bestScore) { bestScore = score; bestRole = role; }
|
|
484
|
+
}
|
|
485
|
+
const cfg2 = getConfig(api);
|
|
486
|
+
const supabaseUrl = cfg2.supabaseUrl || "https://bcudjloikmpcqwcptuyd.supabase.co";
|
|
487
|
+
const supabaseKey = cfg2.supabaseKey || BUILTIN_SUPABASE_ANON_KEY;
|
|
488
|
+
const res = await fetch(`${supabaseUrl}/functions/v1/generate-archetype`, {
|
|
489
|
+
method: "POST",
|
|
490
|
+
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${supabaseKey}` },
|
|
491
|
+
body: JSON.stringify({ archetype: bestRole, profile_text: profileText }),
|
|
492
|
+
});
|
|
493
|
+
if (res.ok) {
|
|
494
|
+
const archData = await res.json();
|
|
495
|
+
if (archData?.reason) archetypeResult = { archetype: bestRole, ...archData };
|
|
496
|
+
}
|
|
471
497
|
}
|
|
472
498
|
} catch {}
|
|
473
499
|
|
|
474
500
|
return ok({
|
|
475
501
|
updated: true,
|
|
476
502
|
profile: { display_name: data.display_name,
|
|
477
|
-
|
|
503
|
+
line1: data.line1, line2: data.line2, line3: data.line3, visible: data.visible },
|
|
478
504
|
public_url: publicUrl,
|
|
479
|
-
|
|
480
|
-
next_step: "Send the public_url
|
|
505
|
+
archetype: archetypeResult || null,
|
|
506
|
+
next_step: "IMPORTANT: 1) Send the public_url to the user — this is their shareable profile link. 2) Tell the user their archetype and the personalized reason. 3) Call antenna_bind to generate a GPS link. Do not skip any step.",
|
|
481
507
|
});
|
|
482
508
|
},
|
|
483
509
|
});
|
|
@@ -504,7 +530,7 @@ export default function register(api: any) {
|
|
|
504
530
|
async execute(_id: string, params: any) {
|
|
505
531
|
const cfg = getConfig(api);
|
|
506
532
|
const supabase = getSupabase(cfg);
|
|
507
|
-
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id
|
|
533
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
|
|
508
534
|
const fuzzy = fuzzyCoords(params.lat, params.lng);
|
|
509
535
|
|
|
510
536
|
// Check if user has a profile first
|
|
@@ -512,7 +538,7 @@ export default function register(api: any) {
|
|
|
512
538
|
if (!profile) {
|
|
513
539
|
return ok({
|
|
514
540
|
checked_in: false,
|
|
515
|
-
message: "
|
|
541
|
+
message: "你还没有名片,别人看到你也不知道你是谁。先创建一个名片吧(告诉我你的名字、emoji、三句话介绍自己)。",
|
|
516
542
|
});
|
|
517
543
|
}
|
|
518
544
|
|
|
@@ -536,7 +562,7 @@ export default function register(api: any) {
|
|
|
536
562
|
api.registerTool({
|
|
537
563
|
name: "antenna_accept",
|
|
538
564
|
description:
|
|
539
|
-
"Accept a match. Use 'ref' from scan results (e.g. '1', '2'), target_device_id, or profile_slug (from profile
|
|
565
|
+
"Accept a match. Use 'ref' from scan results (e.g. '1', '2'), target_device_id, or profile_slug (from a public profile link like antenna.fyi/p/SLUG). Optionally share contact info.",
|
|
540
566
|
parameters: {
|
|
541
567
|
type: "object",
|
|
542
568
|
properties: {
|
|
@@ -544,8 +570,8 @@ export default function register(api: any) {
|
|
|
544
570
|
channel: { type: "string" },
|
|
545
571
|
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." },
|
|
546
572
|
ref: { type: "string", description: "Ref number from scan results (e.g. '1')" },
|
|
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)" },
|
|
573
|
+
target_device_id: { type: "string", description: "Device ID (use ref or profile_slug instead when possible)" },
|
|
574
|
+
profile_slug: { type: "string", description: "Profile slug from a public profile link (e.g. 'yi' from antenna.fyi/p/yi). Resolves to device_id automatically." },
|
|
549
575
|
contact_info: { type: "string", description: "Optional contact info to share" },
|
|
550
576
|
},
|
|
551
577
|
required: ["sender_id", "channel", "chat_id"],
|
|
@@ -553,7 +579,7 @@ export default function register(api: any) {
|
|
|
553
579
|
async execute(_id: string, params: any) {
|
|
554
580
|
const cfg = getConfig(api);
|
|
555
581
|
const supabase = getSupabase(cfg);
|
|
556
|
-
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id
|
|
582
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
|
|
557
583
|
|
|
558
584
|
// Resolve ref to device_id — try DB first, then memory fallback
|
|
559
585
|
let targetId = params.target_device_id;
|
|
@@ -562,13 +588,12 @@ export default function register(api: any) {
|
|
|
562
588
|
const { data: resolved } = await supabase.rpc("resolve_ref", { p_owner: deviceId, p_ref: params.ref });
|
|
563
589
|
targetId = resolved || (api as any)._antennaRefMap?.[params.ref];
|
|
564
590
|
}
|
|
565
|
-
// Resolve profile_slug to device_id
|
|
591
|
+
// Resolve profile_slug to device_id via get_profile_by_slug RPC
|
|
566
592
|
if (!targetId && params.profile_slug) {
|
|
567
|
-
const { data:
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
return ok({ error: `Profile slug "${params.profile_slug}" not found.` });
|
|
593
|
+
const { data: slugProfile } = await supabase.rpc("get_profile_by_slug", { p_slug: params.profile_slug });
|
|
594
|
+
const resolved = Array.isArray(slugProfile) ? slugProfile[0] : slugProfile;
|
|
595
|
+
if (resolved?.device_id) {
|
|
596
|
+
targetId = resolved.device_id;
|
|
572
597
|
}
|
|
573
598
|
}
|
|
574
599
|
if (!targetId) {
|
|
@@ -638,7 +663,7 @@ export default function register(api: any) {
|
|
|
638
663
|
async execute(_id: string, params: any) {
|
|
639
664
|
const cfg = getConfig(api);
|
|
640
665
|
const supabase = getSupabase(cfg);
|
|
641
|
-
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id
|
|
666
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
|
|
642
667
|
|
|
643
668
|
const { data, error } = await supabase.rpc("create_bind_token", {
|
|
644
669
|
p_device_id: deviceId,
|
|
@@ -660,6 +685,45 @@ export default function register(api: any) {
|
|
|
660
685
|
},
|
|
661
686
|
});
|
|
662
687
|
|
|
688
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
689
|
+
// Tool: antenna_link_account
|
|
690
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
691
|
+
api.registerTool({
|
|
692
|
+
name: "antenna_link_account",
|
|
693
|
+
description:
|
|
694
|
+
"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.",
|
|
695
|
+
parameters: {
|
|
696
|
+
type: "object",
|
|
697
|
+
properties: {
|
|
698
|
+
sender_id: { type: "string", description: "The sender's user ID" },
|
|
699
|
+
channel: { type: "string", description: "The channel name" },
|
|
700
|
+
chat_id: { type: "string", description: "REQUIRED. Pass the chat/channel ID from your message context." },
|
|
701
|
+
api_key: { type: "string", description: "The user's Antenna API key (ant_xxx) from antenna.fyi/me" },
|
|
702
|
+
},
|
|
703
|
+
required: ["sender_id", "channel", "chat_id", "api_key"],
|
|
704
|
+
},
|
|
705
|
+
async execute(_id: string, params: any) {
|
|
706
|
+
const cfg = getConfig(api);
|
|
707
|
+
const supabase = getSupabase(cfg);
|
|
708
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
|
|
709
|
+
|
|
710
|
+
const { data, error } = await supabase.rpc("bind_user_id", {
|
|
711
|
+
p_device_id: deviceId,
|
|
712
|
+
p_api_key: params.api_key,
|
|
713
|
+
});
|
|
714
|
+
if (error) return ok({ error: error.message });
|
|
715
|
+
|
|
716
|
+
if (data?.error) {
|
|
717
|
+
return ok(data);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
return ok({
|
|
721
|
+
...data,
|
|
722
|
+
message: "账号已关联!现在你可以在 antenna.fyi/me 看到你的完整 profile 和匹配记录了。",
|
|
723
|
+
});
|
|
724
|
+
},
|
|
725
|
+
});
|
|
726
|
+
|
|
663
727
|
// ═══════════════════════════════════════════════════════════════════
|
|
664
728
|
// Tool: antenna_discover
|
|
665
729
|
// ═══════════════════════════════════════════════════════════════════
|
|
@@ -679,7 +743,7 @@ export default function register(api: any) {
|
|
|
679
743
|
async execute(_id: string, params: any) {
|
|
680
744
|
const cfg = getConfig(api);
|
|
681
745
|
const supabase = getSupabase(cfg);
|
|
682
|
-
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id
|
|
746
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
|
|
683
747
|
|
|
684
748
|
const { data: globalData } = await supabase.rpc("global_discover", {
|
|
685
749
|
p_device_id: deviceId, p_limit: 1,
|
|
@@ -722,7 +786,16 @@ export default function register(api: any) {
|
|
|
722
786
|
} catch { /* best effort */ }
|
|
723
787
|
}
|
|
724
788
|
|
|
725
|
-
profiles.push({
|
|
789
|
+
profiles.push({
|
|
790
|
+
ref: ref,
|
|
791
|
+
name: p.display_name || "匿名",
|
|
792
|
+
personal_description: p.line1,
|
|
793
|
+
looking_for: p.line2,
|
|
794
|
+
conversation_style: p.line3,
|
|
795
|
+
more_information: p.matching_context || null,
|
|
796
|
+
profile_slug: p.profile_slug || null,
|
|
797
|
+
match_reason: match_reason,
|
|
798
|
+
});
|
|
726
799
|
}
|
|
727
800
|
|
|
728
801
|
// Persist refs + log recommendation
|
|
@@ -741,13 +814,86 @@ export default function register(api: any) {
|
|
|
741
814
|
},
|
|
742
815
|
});
|
|
743
816
|
|
|
817
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
818
|
+
// Tool: antenna_find_people
|
|
819
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
820
|
+
api.registerTool({
|
|
821
|
+
name: "antenna_find_people",
|
|
822
|
+
description:
|
|
823
|
+
"Find 1-3 people by a free-form intent, e.g. '想找一个懂 consumer social 增长的人'. Returns privacy-safe refs; use ref with antenna_accept if the user wants an intro.",
|
|
824
|
+
parameters: {
|
|
825
|
+
type: "object",
|
|
826
|
+
properties: {
|
|
827
|
+
query: { type: "string", description: "Free-form user intent describing the kind of person to find" },
|
|
828
|
+
sender_id: { type: "string", description: "The sender's user ID" },
|
|
829
|
+
channel: { type: "string", description: "The channel name" },
|
|
830
|
+
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." },
|
|
831
|
+
limit: { type: "number", description: "Maximum profiles to return, 1-3" },
|
|
832
|
+
},
|
|
833
|
+
required: ["query", "sender_id", "channel", "chat_id"],
|
|
834
|
+
},
|
|
835
|
+
async execute(_id: string, params: any) {
|
|
836
|
+
const cfg = getConfig(api);
|
|
837
|
+
const supabase = getSupabase(cfg);
|
|
838
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
|
|
839
|
+
const query = String(params.query || "").trim();
|
|
840
|
+
const limit = Math.min(Math.max(Number(params.limit) || 3, 1), 3);
|
|
841
|
+
|
|
842
|
+
if (query.length < 2) {
|
|
843
|
+
return ok({ count: 0, profiles: [], message: "Tell me what kind of person you want to find." });
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
const embedding = await generateEmbeddingForQuery(cfg, query);
|
|
847
|
+
const { data, error } = await supabase.rpc("antenna_intent_search_people", {
|
|
848
|
+
p_device_id: deviceId,
|
|
849
|
+
p_query: query,
|
|
850
|
+
p_query_embedding: embedding ? `[${embedding.join(",")}]` : null,
|
|
851
|
+
p_limit: limit,
|
|
852
|
+
});
|
|
853
|
+
if (error) return ok({ error: error.message });
|
|
854
|
+
|
|
855
|
+
const _refMap: Record<string, string> = {};
|
|
856
|
+
const profiles = (data || []).map((p: any, i: number) => {
|
|
857
|
+
const ref = String(i + 1);
|
|
858
|
+
_refMap[ref] = p.device_id;
|
|
859
|
+
return {
|
|
860
|
+
ref: ref,
|
|
861
|
+
display_name: p.display_name || "匿名",
|
|
862
|
+
profile_slug: p.profile_slug || null,
|
|
863
|
+
personal_description: p.personal_description || null,
|
|
864
|
+
looking_for: p.looking_for || null,
|
|
865
|
+
conversation_style: p.conversation_style || null,
|
|
866
|
+
more_information: p.more_information || null,
|
|
867
|
+
interest_tags: p.interest_tags || [],
|
|
868
|
+
city: p.city || null,
|
|
869
|
+
match_score: typeof p.match_score === "number" ? Math.round(p.match_score * 1000) / 1000 : null,
|
|
870
|
+
recommendation_reason: intentSearchReason(query, p),
|
|
871
|
+
};
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
(api as any)._antennaRefMap = { ...(api as any)._antennaRefMap, ..._refMap };
|
|
875
|
+
if (Object.keys(_refMap).length > 0) {
|
|
876
|
+
try { await supabase.rpc("save_scan_refs", { p_owner: deviceId, p_refs: _refMap }); } catch {}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
return ok({
|
|
880
|
+
count: profiles.length,
|
|
881
|
+
profiles,
|
|
882
|
+
query,
|
|
883
|
+
message: profiles.length
|
|
884
|
+
? "Intent search results. Recommend only the best fit(s), then use ref with antenna_accept if the user wants an intro."
|
|
885
|
+
: "No relevant active profiles found for that intent right now.",
|
|
886
|
+
});
|
|
887
|
+
},
|
|
888
|
+
});
|
|
889
|
+
|
|
744
890
|
// ═══════════════════════════════════════════════════════════════════
|
|
745
891
|
// Tool: antenna_initial_recommendations
|
|
746
892
|
// ═══════════════════════════════════════════════════════════════════
|
|
747
893
|
api.registerTool({
|
|
748
894
|
name: "antenna_initial_recommendations",
|
|
749
895
|
description:
|
|
750
|
-
"
|
|
896
|
+
"Get initial recommendations for a new user — 2-3 people most similar to them. One-time only, does NOT consume daily discover quota. Use right after profile creation in onboarding.",
|
|
751
897
|
parameters: {
|
|
752
898
|
type: "object",
|
|
753
899
|
properties: {
|
|
@@ -760,51 +906,27 @@ export default function register(api: any) {
|
|
|
760
906
|
async execute(_id: string, params: any) {
|
|
761
907
|
const cfg = getConfig(api);
|
|
762
908
|
const supabase = getSupabase(cfg);
|
|
763
|
-
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id
|
|
764
|
-
|
|
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
|
-
}
|
|
909
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
|
|
775
910
|
|
|
776
|
-
const { data:
|
|
777
|
-
p_device_id: deviceId,
|
|
911
|
+
const { data: results, error } = await supabase.rpc("initial_recommendations", {
|
|
912
|
+
p_device_id: deviceId,
|
|
913
|
+
p_limit: 3,
|
|
778
914
|
});
|
|
779
915
|
|
|
780
|
-
|
|
916
|
+
if (error) return ok({ error: error.message });
|
|
781
917
|
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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),
|
|
918
|
+
if (!results || results.length === 0) {
|
|
919
|
+
return ok({
|
|
920
|
+
count: 0,
|
|
921
|
+
profiles: [],
|
|
922
|
+
initial: true,
|
|
923
|
+
message: "暂时没有推荐,等有更多人加入!",
|
|
798
924
|
});
|
|
799
|
-
} catch {}
|
|
800
|
-
|
|
801
|
-
if (results.length === 0) {
|
|
802
|
-
return ok({ count: 0, profiles: [], message: "目前还没有足够的用户来匹配。你是早期用户!" });
|
|
803
925
|
}
|
|
804
926
|
|
|
805
927
|
const _refMap: Record<string, string> = {};
|
|
806
928
|
|
|
807
|
-
// Get my profile for match reason
|
|
929
|
+
// Get my profile for match reason generation
|
|
808
930
|
const { data: myProfile } = await supabase.rpc("get_profile", { p_device_id: deviceId });
|
|
809
931
|
const myLines = myProfile ? [myProfile.line1, myProfile.line2, myProfile.line3].filter(Boolean).join(". ") : "";
|
|
810
932
|
|
|
@@ -833,21 +955,21 @@ export default function register(api: any) {
|
|
|
833
955
|
} catch { /* best effort */ }
|
|
834
956
|
}
|
|
835
957
|
|
|
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
|
-
});
|
|
958
|
+
profiles.push({ ref, emoji: p.emoji || "👤", name: p.display_name || "匿名", personal_description: p.line1, looking_for: p.line2, conversation_style: p.line3, more_information: p.matching_context || null, profile_slug: p.profile_slug || null, match_reason });
|
|
840
959
|
}
|
|
841
960
|
|
|
961
|
+
// Persist refs + log recommendations
|
|
842
962
|
(api as any)._antennaRefMap = { ...(api as any)._antennaRefMap, ..._refMap };
|
|
843
|
-
try {
|
|
963
|
+
try {
|
|
964
|
+
await supabase.rpc("save_scan_refs", { p_owner: deviceId, p_refs: _refMap });
|
|
965
|
+
} catch { /* best effort */ }
|
|
844
966
|
for (const p of results) {
|
|
845
|
-
await supabase.rpc("log_recommendation", { p_device_id: deviceId, p_recommended_id: p
|
|
967
|
+
await supabase.rpc("log_recommendation", { p_device_id: deviceId, p_recommended_id: (p as any).device_id });
|
|
846
968
|
}
|
|
847
969
|
|
|
848
970
|
return ok({
|
|
849
971
|
count: profiles.length, profiles, initial: true,
|
|
850
|
-
message: "
|
|
972
|
+
message: "这是你的首次推荐——基于你的名片,这几个人跟你最匹配。",
|
|
851
973
|
});
|
|
852
974
|
},
|
|
853
975
|
});
|
|
@@ -879,11 +1001,11 @@ export default function register(api: any) {
|
|
|
879
1001
|
async execute(_id: string, params: any) {
|
|
880
1002
|
const cfg = getConfig(api);
|
|
881
1003
|
const supabase = getSupabase(cfg);
|
|
882
|
-
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id
|
|
1004
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
|
|
883
1005
|
const { data, error } = await supabase.rpc("create_event", {
|
|
884
1006
|
p_name: params.name,
|
|
885
|
-
p_lat: params.lat
|
|
886
|
-
p_lng: params.lng
|
|
1007
|
+
p_lat: params.lat ?? null,
|
|
1008
|
+
p_lng: params.lng ?? null,
|
|
887
1009
|
p_created_by: deviceId,
|
|
888
1010
|
p_starts_at: params.starts_at || null,
|
|
889
1011
|
p_ends_at: params.ends_at || null,
|
|
@@ -916,7 +1038,7 @@ export default function register(api: any) {
|
|
|
916
1038
|
async execute(_id: string, params: any) {
|
|
917
1039
|
const cfg = getConfig(api);
|
|
918
1040
|
const supabase = getSupabase(cfg);
|
|
919
|
-
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id
|
|
1041
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
|
|
920
1042
|
const { data, error } = await supabase.rpc("end_event", {
|
|
921
1043
|
p_code: params.code,
|
|
922
1044
|
p_device_id: deviceId,
|
|
@@ -948,7 +1070,7 @@ export default function register(api: any) {
|
|
|
948
1070
|
async execute(_id: string, params: any) {
|
|
949
1071
|
const cfg = getConfig(api);
|
|
950
1072
|
const supabase = getSupabase(cfg);
|
|
951
|
-
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id
|
|
1073
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
|
|
952
1074
|
|
|
953
1075
|
// Profile gate
|
|
954
1076
|
const { data: profile } = await supabase.rpc("get_profile", { p_device_id: deviceId });
|
|
@@ -967,7 +1089,7 @@ export default function register(api: any) {
|
|
|
967
1089
|
} catch {}
|
|
968
1090
|
}
|
|
969
1091
|
|
|
970
|
-
const { data, error } = await supabase.rpc("join_event", { p_code: params.code, p_device_id: deviceId, p_lat: lat
|
|
1092
|
+
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 });
|
|
971
1093
|
if (error) return ok({ error: error.message });
|
|
972
1094
|
if (!data?.joined) return ok(data);
|
|
973
1095
|
|
|
@@ -1030,7 +1152,7 @@ export default function register(api: any) {
|
|
|
1030
1152
|
async execute(_id: string, params: any) {
|
|
1031
1153
|
const cfg = getConfig(api);
|
|
1032
1154
|
const supabase = getSupabase(cfg);
|
|
1033
|
-
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id
|
|
1155
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
|
|
1034
1156
|
|
|
1035
1157
|
const { data, error } = await supabase.rpc("event_participants_list", { p_code: params.code, p_device_id: deviceId });
|
|
1036
1158
|
if (error) return ok({ error: error.message });
|
|
@@ -1040,7 +1162,20 @@ export default function register(api: any) {
|
|
|
1040
1162
|
const profiles = others.map((p, i) => {
|
|
1041
1163
|
const ref = String(i + 1);
|
|
1042
1164
|
_refMap[ref] = p.device_id;
|
|
1043
|
-
return {
|
|
1165
|
+
return {
|
|
1166
|
+
ref: ref,
|
|
1167
|
+
name: p.display_name || "匿名",
|
|
1168
|
+
personal_description: p.line1,
|
|
1169
|
+
looking_for: p.line2,
|
|
1170
|
+
conversation_style: p.line3,
|
|
1171
|
+
more_information: p.matching_context || null,
|
|
1172
|
+
profile_slug: p.profile_slug || null,
|
|
1173
|
+
checked_in: !!p.checked_in,
|
|
1174
|
+
role: p.role || "participant",
|
|
1175
|
+
status: p.status || "active",
|
|
1176
|
+
application_context: p.application_context || null,
|
|
1177
|
+
source: "event",
|
|
1178
|
+
};
|
|
1044
1179
|
});
|
|
1045
1180
|
|
|
1046
1181
|
(api as any)._antennaRefMap = { ...(api as any)._antennaRefMap, ..._refMap };
|
|
@@ -1070,7 +1205,7 @@ export default function register(api: any) {
|
|
|
1070
1205
|
async execute(_id: string, params: any) {
|
|
1071
1206
|
const cfg = getConfig(api);
|
|
1072
1207
|
const supabase = getSupabase(cfg);
|
|
1073
|
-
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id
|
|
1208
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
|
|
1074
1209
|
|
|
1075
1210
|
let targetId = params.target_device_id;
|
|
1076
1211
|
if (!targetId && params.ref) {
|
|
@@ -1107,7 +1242,7 @@ export default function register(api: any) {
|
|
|
1107
1242
|
async execute(_id: string, params: any) {
|
|
1108
1243
|
const cfg = getConfig(api);
|
|
1109
1244
|
const supabase = getSupabase(cfg);
|
|
1110
|
-
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id
|
|
1245
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
|
|
1111
1246
|
const fuzzy = (params.lat != null && params.lng != null) ? fuzzyCoords(params.lat, params.lng) : { lat: null, lng: null };
|
|
1112
1247
|
const { data, error } = await supabase.rpc("event_checkin", {
|
|
1113
1248
|
p_code: params.code,
|
|
@@ -1166,7 +1301,7 @@ export default function register(api: any) {
|
|
|
1166
1301
|
async execute(_id: string, params: any) {
|
|
1167
1302
|
const cfg = getConfig(api);
|
|
1168
1303
|
const supabase = getSupabase(cfg);
|
|
1169
|
-
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id
|
|
1304
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
|
|
1170
1305
|
|
|
1171
1306
|
const { data: result } = await supabase.rpc("get_my_matches_with_profiles", { p_device_id: deviceId });
|
|
1172
1307
|
|
|
@@ -1181,18 +1316,18 @@ export default function register(api: any) {
|
|
|
1181
1316
|
ref: String(i + 1),
|
|
1182
1317
|
_device_id: m.target_id,
|
|
1183
1318
|
name: m.name || "匿名",
|
|
1184
|
-
|
|
1185
|
-
|
|
1319
|
+
emoji: m.emoji || "👤",
|
|
1320
|
+
line1: m.line1, line2: m.line2, line3: m.line3,
|
|
1186
1321
|
their_contact: m.their_contact || null,
|
|
1187
1322
|
you_shared: m.you_shared || null,
|
|
1188
1323
|
}));
|
|
1189
1324
|
|
|
1190
1325
|
const incomingAccepts = rawIncoming.map((m: any, i: number) => ({
|
|
1191
|
-
ref: String(i + 1),
|
|
1326
|
+
ref: String(mutualMatches.length + i + 1),
|
|
1192
1327
|
_device_id: m.target_id,
|
|
1193
1328
|
name: m.name || "匿名",
|
|
1194
|
-
|
|
1195
|
-
|
|
1329
|
+
emoji: m.emoji || "👤",
|
|
1330
|
+
line1: m.line1, line2: m.line2, line3: m.line3,
|
|
1196
1331
|
}));
|
|
1197
1332
|
|
|
1198
1333
|
// Clean up follow-up crons for mutual matches
|
|
@@ -1206,6 +1341,14 @@ export default function register(api: any) {
|
|
|
1206
1341
|
if (incomingAccepts.length > 0) messages.push(`${incomingAccepts.length} 个人想认识你,等你回应`);
|
|
1207
1342
|
if (messages.length === 0) messages.push("你接受了一些匹配,但对方还没有回应。耐心等等 ⏳");
|
|
1208
1343
|
|
|
1344
|
+
// Persist ref map so accept(ref) resolves correctly
|
|
1345
|
+
const _refMap: Record<string, string> = {};
|
|
1346
|
+
for (const m of mutualMatches) _refMap[m.ref] = m._device_id;
|
|
1347
|
+
for (const m of incomingAccepts) _refMap[m.ref] = m._device_id;
|
|
1348
|
+
if (deviceId && Object.keys(_refMap).length > 0) {
|
|
1349
|
+
try { await supabase.rpc("save_scan_refs", { p_owner: deviceId, p_refs: _refMap }); } catch { /* best effort */ }
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1209
1352
|
return ok({
|
|
1210
1353
|
mutual_matches: mutualMatches,
|
|
1211
1354
|
incoming_accepts: incomingAccepts,
|
|
@@ -1234,18 +1377,22 @@ export default function register(api: any) {
|
|
|
1234
1377
|
lng: { type: "number", description: "New event longitude" },
|
|
1235
1378
|
starts_at: { type: "string", description: "New start time ISO" },
|
|
1236
1379
|
ends_at: { type: "string", description: "New end time ISO" },
|
|
1380
|
+
requires_approval: { type: "boolean", description: "Require host approval to join" },
|
|
1381
|
+
screening_questions: { type: "array", items: { type: "string" }, description: "Screening questions for applicants" },
|
|
1237
1382
|
},
|
|
1238
1383
|
required: ["code", "sender_id", "channel", "chat_id"],
|
|
1239
1384
|
},
|
|
1240
1385
|
async execute(_id: string, params: any) {
|
|
1241
1386
|
const cfg = getConfig(api);
|
|
1242
1387
|
const supabase = getSupabase(cfg);
|
|
1243
|
-
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id
|
|
1388
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
|
|
1244
1389
|
const { data, error } = await supabase.rpc("update_event", {
|
|
1245
1390
|
p_code: params.code, p_device_id: deviceId,
|
|
1246
1391
|
p_name: params.name || null, p_description: params.description || null,
|
|
1247
|
-
p_og_image: params.og_image || null, p_lat: params.lat
|
|
1392
|
+
p_og_image: params.og_image || null, p_lat: params.lat ?? null, p_lng: params.lng ?? null,
|
|
1248
1393
|
p_starts_at: params.starts_at || null, p_ends_at: params.ends_at || null,
|
|
1394
|
+
...(params.requires_approval != null ? { p_requires_approval: params.requires_approval } : {}),
|
|
1395
|
+
...(params.screening_questions != null ? { p_screening_questions: params.screening_questions } : {}),
|
|
1249
1396
|
});
|
|
1250
1397
|
if (error) return ok({ error: error.message });
|
|
1251
1398
|
return ok(data);
|
|
@@ -1272,7 +1419,7 @@ export default function register(api: any) {
|
|
|
1272
1419
|
async execute(_id: string, params: any) {
|
|
1273
1420
|
const cfg = getConfig(api);
|
|
1274
1421
|
const supabase = getSupabase(cfg);
|
|
1275
|
-
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id
|
|
1422
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
|
|
1276
1423
|
const { data, error } = await supabase.rpc("approve_participant", {
|
|
1277
1424
|
p_code: params.code, p_device_id: deviceId, p_target_ref: params.ref,
|
|
1278
1425
|
});
|
|
@@ -1301,7 +1448,7 @@ export default function register(api: any) {
|
|
|
1301
1448
|
async execute(_id: string, params: any) {
|
|
1302
1449
|
const cfg = getConfig(api);
|
|
1303
1450
|
const supabase = getSupabase(cfg);
|
|
1304
|
-
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id
|
|
1451
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
|
|
1305
1452
|
const { data, error } = await supabase.rpc("reject_participant", {
|
|
1306
1453
|
p_code: params.code, p_device_id: deviceId, p_target_ref: params.ref,
|
|
1307
1454
|
});
|
|
@@ -1330,7 +1477,7 @@ export default function register(api: any) {
|
|
|
1330
1477
|
async execute(_id: string, params: any) {
|
|
1331
1478
|
const cfg = getConfig(api);
|
|
1332
1479
|
const supabase = getSupabase(cfg);
|
|
1333
|
-
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id
|
|
1480
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
|
|
1334
1481
|
const { data, error } = await supabase.rpc("add_cohost", {
|
|
1335
1482
|
p_code: params.code, p_device_id: deviceId, p_target_ref: params.ref,
|
|
1336
1483
|
});
|
|
@@ -1344,72 +1491,34 @@ export default function register(api: any) {
|
|
|
1344
1491
|
// ═══════════════════════════════════════════════════════════════════
|
|
1345
1492
|
api.registerTool({
|
|
1346
1493
|
name: "antenna_event_message",
|
|
1347
|
-
description:
|
|
1348
|
-
"Send a message to event participants. Only creator or co-host can send. Omit ref to broadcast to all.",
|
|
1494
|
+
description: "Send a message to event participants. Only creator or co-host can send. Omit ref to broadcast to all participants.",
|
|
1349
1495
|
parameters: {
|
|
1350
1496
|
type: "object",
|
|
1351
1497
|
properties: {
|
|
1352
1498
|
code: { type: "string", description: "Event code" },
|
|
1353
|
-
sender_id: { type: "string"
|
|
1354
|
-
channel: { type: "string"
|
|
1499
|
+
sender_id: { type: "string" },
|
|
1500
|
+
channel: { type: "string" },
|
|
1355
1501
|
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." },
|
|
1356
|
-
message: { type: "string", description: "Message to send" },
|
|
1357
|
-
ref: { type: "string", description: "Ref number of specific participant (omit
|
|
1502
|
+
message: { type: "string", description: "Message to send to participants" },
|
|
1503
|
+
ref: { type: "string", description: "Ref number of specific participant (omit to broadcast to all)" },
|
|
1358
1504
|
},
|
|
1359
|
-
required: ["code", "sender_id", "channel", "
|
|
1505
|
+
required: ["code", "sender_id", "channel", "message", "chat_id"],
|
|
1360
1506
|
},
|
|
1361
1507
|
async execute(_id: string, params: any) {
|
|
1362
1508
|
const cfg = getConfig(api);
|
|
1363
1509
|
const supabase = getSupabase(cfg);
|
|
1364
|
-
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id
|
|
1365
|
-
const
|
|
1510
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
|
|
1511
|
+
const { data, error } = await supabase.rpc("send_event_message", {
|
|
1366
1512
|
p_code: params.code,
|
|
1367
1513
|
p_device_id: deviceId,
|
|
1368
1514
|
p_message: params.message,
|
|
1369
|
-
|
|
1370
|
-
if (params.ref) rpcParams.p_target_ref = params.ref;
|
|
1371
|
-
const { data, error } = await supabase.rpc("send_event_message", rpcParams);
|
|
1372
|
-
if (error) return ok({ error: error.message });
|
|
1373
|
-
return ok(data);
|
|
1374
|
-
},
|
|
1375
|
-
});
|
|
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,
|
|
1515
|
+
...(params.ref ? { p_target_ref: params.ref } : {}),
|
|
1401
1516
|
});
|
|
1402
1517
|
if (error) return ok({ error: error.message });
|
|
1403
|
-
|
|
1404
|
-
return ok({
|
|
1405
|
-
...data,
|
|
1406
|
-
message: "账号已关联!现在你可以在 antenna.fyi/me 看到你的完整 profile 和匹配记录了。",
|
|
1407
|
-
});
|
|
1518
|
+
return ok(data);
|
|
1408
1519
|
},
|
|
1409
1520
|
});
|
|
1410
1521
|
|
|
1411
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
1412
|
-
// Service: poll for new matches every 10 minutes → notify instantly
|
|
1413
1522
|
// ═══════════════════════════════════════════════════════════════════
|
|
1414
1523
|
const _notifiedMatches = new Set<string>(); // "deviceA→deviceB" already notified
|
|
1415
1524
|
|
|
@@ -1432,11 +1541,10 @@ export default function register(api: any) {
|
|
|
1432
1541
|
async (payload: any) => {
|
|
1433
1542
|
try {
|
|
1434
1543
|
const targetDeviceId = payload.new?.device_id_b;
|
|
1435
|
-
if (!targetDeviceId
|
|
1544
|
+
if (!targetDeviceId) return;
|
|
1436
1545
|
|
|
1437
1546
|
const key = `${payload.new.device_id_a}→${targetDeviceId}`;
|
|
1438
1547
|
if (_notifiedMatches.has(key)) return;
|
|
1439
|
-
_notifiedMatches.add(key);
|
|
1440
1548
|
|
|
1441
1549
|
const parts = targetDeviceId.split(":");
|
|
1442
1550
|
if (parts.length < 2) return;
|
|
@@ -1448,6 +1556,7 @@ export default function register(api: any) {
|
|
|
1448
1556
|
|
|
1449
1557
|
const { data: theirProfile } = await innerSb.rpc("get_profile", { p_device_id: payload.new.device_id_a });
|
|
1450
1558
|
const name = theirProfile?.display_name || "有人";
|
|
1559
|
+
const emoji = theirProfile?.emoji || "👤";
|
|
1451
1560
|
|
|
1452
1561
|
// Check if mutual
|
|
1453
1562
|
const { data: matches } = await innerSb.rpc("get_my_matches", { p_device_id: targetDeviceId });
|
|
@@ -1458,13 +1567,15 @@ export default function register(api: any) {
|
|
|
1458
1567
|
if (myAccept) {
|
|
1459
1568
|
const contact = payload.new.contact_info_a ? `\n对方的联系方式:${payload.new.contact_info_a}` : "";
|
|
1460
1569
|
notifyUser(channel, userId,
|
|
1461
|
-
`[Antenna] 🎉 双向匹配!${name} 也接受了你!${contact}\n\n用 antenna_check_matches 查看详情。`,
|
|
1462
|
-
logger
|
|
1570
|
+
`[Antenna] 🎉 双向匹配!${emoji} ${name} 也接受了你!${contact}\n\n用 antenna_check_matches 查看详情。`,
|
|
1571
|
+
logger);
|
|
1572
|
+
_notifiedMatches.add(key);
|
|
1463
1573
|
stopFollowUpCron(targetDeviceId, payload.new.device_id_a, logger);
|
|
1464
1574
|
} else {
|
|
1465
1575
|
notifyUser(channel, userId,
|
|
1466
|
-
`[Antenna] 📩 ${name} 想认识你!看看 TA 的名片,决定要不要接受?\n\n用 antenna_check_matches 查看详情。`,
|
|
1467
|
-
logger
|
|
1576
|
+
`[Antenna] 📩 ${emoji} ${name} 想认识你!看看 TA 的名片,决定要不要接受?\n\n用 antenna_check_matches 查看详情。`,
|
|
1577
|
+
logger);
|
|
1578
|
+
_notifiedMatches.add(key);
|
|
1468
1579
|
}
|
|
1469
1580
|
} catch (err: any) {
|
|
1470
1581
|
logger.warn("Antenna: realtime match handler error:", err.message);
|
|
@@ -1501,12 +1612,13 @@ export default function register(api: any) {
|
|
|
1501
1612
|
// Get applicant profile
|
|
1502
1613
|
const { data: applicant } = await epSb.rpc('get_profile', { p_device_id: applicantDeviceId });
|
|
1503
1614
|
const aName = applicant?.display_name || '某人';
|
|
1615
|
+
const aEmoji = applicant?.emoji || '👤';
|
|
1504
1616
|
|
|
1505
1617
|
const parts = event.created_by.split(':');
|
|
1506
1618
|
if (parts.length < 2) return;
|
|
1507
1619
|
notifyUser(parts[0], parts.slice(1).join(':'),
|
|
1508
|
-
`[Antenna] 📩 ${aName} 申请加入你的活动「${event.name}」\n\n用 antenna_event_scan --code ${event.code} 查看申请者名片并审批。`,
|
|
1509
|
-
logger
|
|
1620
|
+
`[Antenna] 📩 ${aEmoji} ${aName} 申请加入你的活动「${event.name}」\n\n用 antenna_event_scan --code ${event.code} 查看申请者名片并审批。`,
|
|
1621
|
+
logger);
|
|
1510
1622
|
} catch (err: any) {
|
|
1511
1623
|
logger.warn('Antenna: event participant INSERT handler error:', err.message);
|
|
1512
1624
|
}
|
|
@@ -1535,11 +1647,11 @@ export default function register(api: any) {
|
|
|
1535
1647
|
if (newStatus === 'active') {
|
|
1536
1648
|
notifyUser(parts[0], parts.slice(1).join(':'),
|
|
1537
1649
|
`[Antenna] ✅ 你的申请已通过!欢迎加入「${eventName}」\n\n用 antenna_event_scan --code ${event?.code} 查看其他参与者。`,
|
|
1538
|
-
logger
|
|
1650
|
+
logger);
|
|
1539
1651
|
} else if (newStatus === 'rejected') {
|
|
1540
1652
|
notifyUser(parts[0], parts.slice(1).join(':'),
|
|
1541
1653
|
`[Antenna] ❌ 你的申请未通过「${eventName}」的审核。`,
|
|
1542
|
-
logger
|
|
1654
|
+
logger);
|
|
1543
1655
|
}
|
|
1544
1656
|
} catch (err: any) {
|
|
1545
1657
|
logger.warn('Antenna: event participant UPDATE handler error:', err.message);
|
|
@@ -1559,10 +1671,9 @@ export default function register(api: any) {
|
|
|
1559
1671
|
const cfg = getConfig(api);
|
|
1560
1672
|
const supabase = getSupabase(cfg);
|
|
1561
1673
|
|
|
1562
|
-
// Get all profiles
|
|
1674
|
+
// Get all profiles with valid notification targets
|
|
1563
1675
|
const { data: activeProfiles } = await supabase
|
|
1564
|
-
.rpc("
|
|
1565
|
-
.select("device_id");
|
|
1676
|
+
.rpc("get_notification_targets", { p_since: "7 days" });
|
|
1566
1677
|
|
|
1567
1678
|
if (!activeProfiles?.length) return;
|
|
1568
1679
|
|
|
@@ -1592,7 +1703,7 @@ export default function register(api: any) {
|
|
|
1592
1703
|
|
|
1593
1704
|
for (const match of newMatches) {
|
|
1594
1705
|
const notifyKey = `${match.device_id_a}→${match.device_id_b}`;
|
|
1595
|
-
_notifiedMatches.
|
|
1706
|
+
if (_notifiedMatches.has(notifyKey)) continue;
|
|
1596
1707
|
|
|
1597
1708
|
// Is this a new mutual match?
|
|
1598
1709
|
if (match.device_id_a === deviceId) {
|
|
@@ -1600,36 +1711,38 @@ export default function register(api: any) {
|
|
|
1600
1711
|
if (reverse) {
|
|
1601
1712
|
const { data: theirProfile } = await supabase.rpc("get_profile", { p_device_id: match.device_id_b });
|
|
1602
1713
|
const name = theirProfile?.display_name || "对方";
|
|
1714
|
+
const emoji = theirProfile?.emoji || "👤";
|
|
1603
1715
|
const contact = reverse.contact_info_a ? `\n对方的联系方式:${reverse.contact_info_a}` : "";
|
|
1604
1716
|
notifyUser(
|
|
1605
1717
|
channel, userId,
|
|
1606
|
-
`[Antenna] 🎉 双向匹配成功!${name} 也接受了你!${contact}\n\n用 antenna_check_matches 查看详情。`,
|
|
1607
|
-
logger,
|
|
1718
|
+
`[Antenna] 🎉 双向匹配成功!${emoji} ${name} 也接受了你!${contact}\n\n用 antenna_check_matches 查看详情。`,
|
|
1719
|
+
logger,
|
|
1608
1720
|
);
|
|
1609
|
-
|
|
1721
|
+
_notifiedMatches.add(notifyKey);
|
|
1610
1722
|
stopFollowUpCron(deviceId, match.device_id_b, logger);
|
|
1611
1723
|
}
|
|
1612
1724
|
} else if (match.device_id_b === deviceId) {
|
|
1613
1725
|
// Someone new accepted me
|
|
1614
1726
|
const { data: theirProfile } = await supabase.rpc("get_profile", { p_device_id: match.device_id_a });
|
|
1615
1727
|
const name = theirProfile?.display_name || "有人";
|
|
1728
|
+
const emoji = theirProfile?.emoji || "👤";
|
|
1616
1729
|
const iAccepted = myMatches.find((m: any) => m.device_id_b === match.device_id_a);
|
|
1617
1730
|
if (iAccepted) {
|
|
1618
|
-
// I already accepted them → mutual!
|
|
1619
1731
|
const contact = match.contact_info_a ? `\n对方的联系方式:${match.contact_info_a}` : "";
|
|
1620
1732
|
notifyUser(
|
|
1621
1733
|
channel, userId,
|
|
1622
|
-
`[Antenna] 🎉 双向匹配成功!${name} 也接受了你!${contact}\n\n用 antenna_check_matches 查看详情。`,
|
|
1623
|
-
logger,
|
|
1734
|
+
`[Antenna] 🎉 双向匹配成功!${emoji} ${name} 也接受了你!${contact}\n\n用 antenna_check_matches 查看详情。`,
|
|
1735
|
+
logger,
|
|
1624
1736
|
);
|
|
1737
|
+
_notifiedMatches.add(notifyKey);
|
|
1625
1738
|
stopFollowUpCron(deviceId, match.device_id_a, logger);
|
|
1626
1739
|
} else {
|
|
1627
|
-
// They accepted me but I haven't responded
|
|
1628
1740
|
notifyUser(
|
|
1629
1741
|
channel, userId,
|
|
1630
|
-
`[Antenna] 📩 ${name} 想认识你!看看 TA 的名片,决定要不要接受?\n\n用 antenna_check_matches 查看详情。`,
|
|
1631
|
-
logger,
|
|
1742
|
+
`[Antenna] 📩 ${emoji} ${name} 想认识你!看看 TA 的名片,决定要不要接受?\n\n用 antenna_check_matches 查看详情。`,
|
|
1743
|
+
logger,
|
|
1632
1744
|
);
|
|
1745
|
+
_notifiedMatches.add(notifyKey);
|
|
1633
1746
|
}
|
|
1634
1747
|
}
|
|
1635
1748
|
}
|
|
@@ -1640,8 +1753,9 @@ export default function register(api: any) {
|
|
|
1640
1753
|
}
|
|
1641
1754
|
}
|
|
1642
1755
|
|
|
1643
|
-
// ── Event approval polling ──
|
|
1644
|
-
for (const
|
|
1756
|
+
// ── Event approval polling (use notification targets, not _knownDeviceIds) ──
|
|
1757
|
+
for (const profile of activeProfiles) {
|
|
1758
|
+
const deviceId = profile.device_id;
|
|
1645
1759
|
try {
|
|
1646
1760
|
const { data: events } = await supabase.rpc("get_my_event_updates", { p_device_id: deviceId });
|
|
1647
1761
|
if (!events?.length) continue;
|
|
@@ -1650,23 +1764,47 @@ export default function register(api: any) {
|
|
|
1650
1764
|
const channel = parts[0];
|
|
1651
1765
|
const userId = parts.slice(1).join(":");
|
|
1652
1766
|
for (const ev of events) {
|
|
1653
|
-
const key = `event:${ev.event_id}:${ev.status}`;
|
|
1767
|
+
const key = `event:${deviceId}:${ev.event_id}:${ev.status}`;
|
|
1654
1768
|
if (_notifiedMatches.has(key)) continue;
|
|
1655
|
-
|
|
1656
|
-
if (ev.status === "active" && ev.role !== "creator" && ev.role !== "cohost") {
|
|
1769
|
+
if (ev.status === "active" && ev.role !== "creator" && ev.role !== "cohost" && ev.requires_approval) {
|
|
1657
1770
|
notifyUser(channel, userId,
|
|
1658
1771
|
`[Antenna] ✅ 你的申请已通过!欢迎加入「${ev.event_name}」`,
|
|
1659
|
-
logger,
|
|
1772
|
+
logger,
|
|
1660
1773
|
);
|
|
1774
|
+
_notifiedMatches.add(key);
|
|
1661
1775
|
} else if (ev.status === "rejected") {
|
|
1662
1776
|
notifyUser(channel, userId,
|
|
1663
1777
|
`[Antenna] ❌ 你的申请未通过「${ev.event_name}」`,
|
|
1664
|
-
logger,
|
|
1778
|
+
logger,
|
|
1665
1779
|
);
|
|
1780
|
+
_notifiedMatches.add(key);
|
|
1666
1781
|
}
|
|
1667
1782
|
}
|
|
1668
1783
|
} catch { /* silent */ }
|
|
1669
1784
|
}
|
|
1785
|
+
|
|
1786
|
+
// ── Event messages polling ──
|
|
1787
|
+
for (const profile of activeProfiles) {
|
|
1788
|
+
const deviceId = profile.device_id;
|
|
1789
|
+
try {
|
|
1790
|
+
const { data: msgs } = await supabase.rpc("get_my_event_messages", { p_device_id: deviceId });
|
|
1791
|
+
if (!msgs?.length) continue;
|
|
1792
|
+
const parts = deviceId.split(":");
|
|
1793
|
+
if (parts.length < 2) continue;
|
|
1794
|
+
const channel = parts[0];
|
|
1795
|
+
const userId = parts.slice(1).join(":");
|
|
1796
|
+
for (const msg of msgs) {
|
|
1797
|
+
const key = `evtmsg:${msg.event_id}:${msg.created_at}`;
|
|
1798
|
+
if (_notifiedMatches.has(key)) continue;
|
|
1799
|
+
const role = msg.sender_role === 'creator' ? '组织者' : '协办';
|
|
1800
|
+
notifyUser(channel, userId,
|
|
1801
|
+
`[Antenna] 📢 来自「${msg.event_name}」${role} ${msg.sender_emoji || ''} ${msg.sender_name}: ${msg.message}`,
|
|
1802
|
+
logger,
|
|
1803
|
+
);
|
|
1804
|
+
_notifiedMatches.add(key);
|
|
1805
|
+
}
|
|
1806
|
+
} catch { /* silent */ }
|
|
1807
|
+
}
|
|
1670
1808
|
} catch (err: any) {
|
|
1671
1809
|
logger.warn("Antenna: match poll error:", err.message);
|
|
1672
1810
|
}
|
|
@@ -1686,171 +1824,6 @@ export default function register(api: any) {
|
|
|
1686
1824
|
},
|
|
1687
1825
|
});
|
|
1688
1826
|
|
|
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
|
-
|
|
1854
1827
|
// ═══════════════════════════════════════════════════════════════════
|
|
1855
1828
|
// Hook: auto-scan when location is received
|
|
1856
1829
|
// ═══════════════════════════════════════════════════════════════════
|