antenna-openclaw-plugin 1.3.1 → 1.3.3
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 +56 -70
- package/package.json +1 -1
- package/skills/antenna/SKILL.md +109 -121
package/index.ts
CHANGED
|
@@ -268,7 +268,7 @@ export default function register(api: any) {
|
|
|
268
268
|
channel: { type: "string", description: "The channel name" },
|
|
269
269
|
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." },
|
|
270
270
|
},
|
|
271
|
-
required: ["sender_id", "channel"],
|
|
271
|
+
required: ["sender_id", "channel", "chat_id"],
|
|
272
272
|
},
|
|
273
273
|
async execute(_id: string, params: any) {
|
|
274
274
|
const cfg = getConfig(api);
|
|
@@ -277,7 +277,7 @@ export default function register(api: any) {
|
|
|
277
277
|
const radius = params.radius_m ?? cfg.defaultRadiusM ?? 500;
|
|
278
278
|
|
|
279
279
|
if (isRateLimited(deviceId)) {
|
|
280
|
-
return ok({
|
|
280
|
+
return ok({ profiles: [], message: "刚刚才扫描过,稍等一会儿再试。", rate_limited: true });
|
|
281
281
|
}
|
|
282
282
|
|
|
283
283
|
let lat = params.lat;
|
|
@@ -290,7 +290,7 @@ export default function register(api: any) {
|
|
|
290
290
|
lat = loc.lat;
|
|
291
291
|
lng = loc.lng;
|
|
292
292
|
} else {
|
|
293
|
-
return ok({
|
|
293
|
+
return ok({ profiles: [], message: "还没有位置信息。请先通过链接分享位置,或者发送位置消息。" });
|
|
294
294
|
}
|
|
295
295
|
}
|
|
296
296
|
|
|
@@ -323,11 +323,11 @@ export default function register(api: any) {
|
|
|
323
323
|
try { await supabase.rpc("log_recommendation", { p_device_id: deviceId, p_recommended_id: p.device_id }); } catch {}
|
|
324
324
|
}
|
|
325
325
|
return ok({
|
|
326
|
-
|
|
326
|
+
profiles: gProfiles, count: gProfiles.length, radius_m: radius, global: true,
|
|
327
327
|
message: `附近 ${radius}m 暂时没人。今天的全球推荐——这个人跟你可能聊得来。(每天 1 次)`,
|
|
328
328
|
});
|
|
329
329
|
}
|
|
330
|
-
return ok({
|
|
330
|
+
return ok({ profiles: [], message: `附近暂时没人,今天的全球推荐已经用完了。明天再来!` });
|
|
331
331
|
}
|
|
332
332
|
|
|
333
333
|
// Build ref mapping — never expose device_id
|
|
@@ -353,8 +353,8 @@ export default function register(api: any) {
|
|
|
353
353
|
} catch { /* best effort */ }
|
|
354
354
|
|
|
355
355
|
return ok({
|
|
356
|
-
|
|
357
|
-
|
|
356
|
+
profiles: profiles,
|
|
357
|
+
count: others.length,
|
|
358
358
|
radius_m: radius,
|
|
359
359
|
instruction: "根据你对用户的了解,判断哪些人值得推荐,用 ref 编号引用。不要显示 device_id。",
|
|
360
360
|
});
|
|
@@ -381,8 +381,9 @@ export default function register(api: any) {
|
|
|
381
381
|
line2: { type: "string", description: "Second line (what you're into)" },
|
|
382
382
|
line3: { type: "string", description: "Third line (what you're looking for)" },
|
|
383
383
|
visible: { type: "boolean", description: "Whether to be visible to others" },
|
|
384
|
+
matching_context: { type: "string", description: "Free-form context for AI matching (interests, goals, etc.)" },
|
|
384
385
|
},
|
|
385
|
-
required: ["action", "sender_id", "channel"],
|
|
386
|
+
required: ["action", "sender_id", "channel", "chat_id"],
|
|
386
387
|
},
|
|
387
388
|
async execute(_id: string, params: any) {
|
|
388
389
|
const cfg = getConfig(api);
|
|
@@ -406,6 +407,7 @@ export default function register(api: any) {
|
|
|
406
407
|
p_display_name: params.display_name ?? null, p_emoji: params.emoji ?? null,
|
|
407
408
|
p_line1: params.line1 ?? null, p_line2: params.line2 ?? null,
|
|
408
409
|
p_line3: params.line3 ?? null, p_visible: params.visible ?? true,
|
|
410
|
+
...(params.matching_context != null ? { p_matching_context: params.matching_context } : {}),
|
|
409
411
|
});
|
|
410
412
|
|
|
411
413
|
if (error) return ok({ error: error.message });
|
|
@@ -436,7 +438,7 @@ export default function register(api: any) {
|
|
|
436
438
|
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." },
|
|
437
439
|
place_name: { type: "string", description: "Optional: name of the place (for confirmation message)" },
|
|
438
440
|
},
|
|
439
|
-
required: ["lat", "lng", "sender_id", "channel"],
|
|
441
|
+
required: ["lat", "lng", "sender_id", "channel", "chat_id"],
|
|
440
442
|
},
|
|
441
443
|
async execute(_id: string, params: any) {
|
|
442
444
|
const cfg = getConfig(api);
|
|
@@ -484,7 +486,7 @@ export default function register(api: any) {
|
|
|
484
486
|
target_device_id: { type: "string", description: "Device ID (use ref instead when possible)" },
|
|
485
487
|
contact_info: { type: "string", description: "Optional contact info to share" },
|
|
486
488
|
},
|
|
487
|
-
required: ["sender_id", "channel"],
|
|
489
|
+
required: ["sender_id", "channel", "chat_id"],
|
|
488
490
|
},
|
|
489
491
|
async execute(_id: string, params: any) {
|
|
490
492
|
const cfg = getConfig(api);
|
|
@@ -560,7 +562,7 @@ export default function register(api: any) {
|
|
|
560
562
|
purpose: { type: "string", description: "'profile' (default) or 'event'" },
|
|
561
563
|
event_code: { type: "string", description: "Event code (required when purpose=event)" },
|
|
562
564
|
},
|
|
563
|
-
required: ["sender_id", "channel"],
|
|
565
|
+
required: ["sender_id", "channel", "chat_id"],
|
|
564
566
|
},
|
|
565
567
|
async execute(_id: string, params: any) {
|
|
566
568
|
const cfg = getConfig(api);
|
|
@@ -601,7 +603,7 @@ export default function register(api: any) {
|
|
|
601
603
|
channel: { type: "string", description: "The channel name" },
|
|
602
604
|
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." },
|
|
603
605
|
},
|
|
604
|
-
required: ["sender_id", "channel"],
|
|
606
|
+
required: ["sender_id", "channel", "chat_id"],
|
|
605
607
|
},
|
|
606
608
|
async execute(_id: string, params: any) {
|
|
607
609
|
const cfg = getConfig(api);
|
|
@@ -690,7 +692,7 @@ export default function register(api: any) {
|
|
|
690
692
|
requires_approval: { type: "boolean", description: "Require host approval to join (default false)" },
|
|
691
693
|
screening_questions: { type: "array", items: { type: "string" }, description: "Screening questions for applicants" },
|
|
692
694
|
},
|
|
693
|
-
required: ["name", "sender_id", "channel", "starts_at", "ends_at"],
|
|
695
|
+
required: ["name", "sender_id", "channel", "starts_at", "ends_at", "chat_id"],
|
|
694
696
|
},
|
|
695
697
|
async execute(_id: string, params: any) {
|
|
696
698
|
const cfg = getConfig(api);
|
|
@@ -698,8 +700,8 @@ export default function register(api: any) {
|
|
|
698
700
|
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
|
|
699
701
|
const { data, error } = await supabase.rpc("create_event", {
|
|
700
702
|
p_name: params.name,
|
|
701
|
-
p_lat: params.lat
|
|
702
|
-
p_lng: params.lng
|
|
703
|
+
p_lat: params.lat ?? null,
|
|
704
|
+
p_lng: params.lng ?? null,
|
|
703
705
|
p_created_by: deviceId,
|
|
704
706
|
p_starts_at: params.starts_at || null,
|
|
705
707
|
p_ends_at: params.ends_at || null,
|
|
@@ -727,7 +729,7 @@ export default function register(api: any) {
|
|
|
727
729
|
channel: { type: "string" },
|
|
728
730
|
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." },
|
|
729
731
|
},
|
|
730
|
-
required: ["code", "sender_id", "channel"],
|
|
732
|
+
required: ["code", "sender_id", "channel", "chat_id"],
|
|
731
733
|
},
|
|
732
734
|
async execute(_id: string, params: any) {
|
|
733
735
|
const cfg = getConfig(api);
|
|
@@ -759,7 +761,7 @@ export default function register(api: any) {
|
|
|
759
761
|
lng: { type: "number", description: "Longitude (optional, for auto-checkin)" },
|
|
760
762
|
application_context: { type: "string", description: "Application context from screening conversation" },
|
|
761
763
|
},
|
|
762
|
-
required: ["code", "sender_id", "channel"],
|
|
764
|
+
required: ["code", "sender_id", "channel", "chat_id"],
|
|
763
765
|
},
|
|
764
766
|
async execute(_id: string, params: any) {
|
|
765
767
|
const cfg = getConfig(api);
|
|
@@ -783,7 +785,7 @@ export default function register(api: any) {
|
|
|
783
785
|
} catch {}
|
|
784
786
|
}
|
|
785
787
|
|
|
786
|
-
const { data, error } = await supabase.rpc("join_event", { p_code: params.code, p_device_id: deviceId, p_application_context: params.application_context || null });
|
|
788
|
+
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 });
|
|
787
789
|
if (error) return ok({ error: error.message });
|
|
788
790
|
if (!data?.joined) return ok(data);
|
|
789
791
|
|
|
@@ -841,7 +843,7 @@ export default function register(api: any) {
|
|
|
841
843
|
channel: { type: "string" },
|
|
842
844
|
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." },
|
|
843
845
|
},
|
|
844
|
-
required: ["code", "sender_id", "channel"],
|
|
846
|
+
required: ["code", "sender_id", "channel", "chat_id"],
|
|
845
847
|
},
|
|
846
848
|
async execute(_id: string, params: any) {
|
|
847
849
|
const cfg = getConfig(api);
|
|
@@ -881,7 +883,7 @@ export default function register(api: any) {
|
|
|
881
883
|
ref: { type: "string", description: "Ref number from scan/discover results" },
|
|
882
884
|
target_device_id: { type: "string", description: "Device ID (use ref instead when possible)" },
|
|
883
885
|
},
|
|
884
|
-
required: ["sender_id", "channel"],
|
|
886
|
+
required: ["sender_id", "channel", "chat_id"],
|
|
885
887
|
},
|
|
886
888
|
async execute(_id: string, params: any) {
|
|
887
889
|
const cfg = getConfig(api);
|
|
@@ -918,7 +920,7 @@ export default function register(api: any) {
|
|
|
918
920
|
lat: { type: "number", description: "Latitude (optional)" },
|
|
919
921
|
lng: { type: "number", description: "Longitude (optional)" },
|
|
920
922
|
},
|
|
921
|
-
required: ["code", "sender_id", "channel"],
|
|
923
|
+
required: ["code", "sender_id", "channel", "chat_id"],
|
|
922
924
|
},
|
|
923
925
|
async execute(_id: string, params: any) {
|
|
924
926
|
const cfg = getConfig(api);
|
|
@@ -977,60 +979,44 @@ export default function register(api: any) {
|
|
|
977
979
|
channel: { type: "string" },
|
|
978
980
|
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." },
|
|
979
981
|
},
|
|
980
|
-
required: ["sender_id", "channel"],
|
|
982
|
+
required: ["sender_id", "channel", "chat_id"],
|
|
981
983
|
},
|
|
982
984
|
async execute(_id: string, params: any) {
|
|
983
985
|
const cfg = getConfig(api);
|
|
984
986
|
const supabase = getSupabase(cfg);
|
|
985
987
|
const deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
|
|
986
988
|
|
|
987
|
-
const { data:
|
|
989
|
+
const { data: result } = await supabase.rpc("get_my_matches_with_profiles", { p_device_id: deviceId });
|
|
988
990
|
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
}
|
|
991
|
+
const rawMutual = result?.mutual_matches || [];
|
|
992
|
+
const rawIncoming = result?.incoming_accepts || [];
|
|
992
993
|
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
// Matches where someone else accepted me
|
|
996
|
-
const incomingMatches = allMatches.filter((m: any) => m.device_id_b === deviceId);
|
|
997
|
-
|
|
998
|
-
// --- Mutual matches (both sides accepted) ---
|
|
999
|
-
const mutualMatches = [];
|
|
1000
|
-
for (const match of myMatches) {
|
|
1001
|
-
const reverse = incomingMatches.find(
|
|
1002
|
-
(m: any) => m.device_id_a === match.device_id_b
|
|
1003
|
-
);
|
|
1004
|
-
if (reverse) {
|
|
1005
|
-
// Clean up follow-up crons for this mutual pair
|
|
1006
|
-
stopFollowUpCron(deviceId, match.device_id_b, logger);
|
|
1007
|
-
stopFollowUpCron(match.device_id_b, deviceId, logger);
|
|
1008
|
-
|
|
1009
|
-
const { data: profile } = await supabase.rpc("get_profile", { p_device_id: match.device_id_b });
|
|
1010
|
-
mutualMatches.push({
|
|
1011
|
-
device_id: match.device_id_b,
|
|
1012
|
-
name: profile?.display_name || "匿名", emoji: profile?.emoji || "👤",
|
|
1013
|
-
line1: profile?.line1, line2: profile?.line2, line3: profile?.line3,
|
|
1014
|
-
their_contact: reverse.contact_info_a || null, you_shared: match.contact_info_a || null,
|
|
1015
|
-
});
|
|
1016
|
-
}
|
|
994
|
+
if (!rawMutual.length && !rawIncoming.length) {
|
|
995
|
+
return ok({ mutual_matches: [], incoming_accepts: [], message: "目前没有进行中的匹配。" });
|
|
1017
996
|
}
|
|
1018
997
|
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
998
|
+
const mutualMatches = rawMutual.map((m: any, i: number) => ({
|
|
999
|
+
ref: String(i + 1),
|
|
1000
|
+
_device_id: m.target_id,
|
|
1001
|
+
name: m.name || "匿名",
|
|
1002
|
+
emoji: m.emoji || "👤",
|
|
1003
|
+
line1: m.line1, line2: m.line2, line3: m.line3,
|
|
1004
|
+
their_contact: m.their_contact || null,
|
|
1005
|
+
you_shared: m.you_shared || null,
|
|
1006
|
+
}));
|
|
1007
|
+
|
|
1008
|
+
const incomingAccepts = rawIncoming.map((m: any, i: number) => ({
|
|
1009
|
+
ref: String(i + 1),
|
|
1010
|
+
_device_id: m.target_id,
|
|
1011
|
+
name: m.name || "匿名",
|
|
1012
|
+
emoji: m.emoji || "👤",
|
|
1013
|
+
line1: m.line1, line2: m.line2, line3: m.line3,
|
|
1014
|
+
}));
|
|
1015
|
+
|
|
1016
|
+
// Clean up follow-up crons for mutual matches
|
|
1017
|
+
for (const m of mutualMatches) {
|
|
1018
|
+
stopFollowUpCron(deviceId, m._device_id, logger);
|
|
1019
|
+
stopFollowUpCron(m._device_id, deviceId, logger);
|
|
1034
1020
|
}
|
|
1035
1021
|
|
|
1036
1022
|
const messages = [];
|
|
@@ -1067,7 +1053,7 @@ export default function register(api: any) {
|
|
|
1067
1053
|
starts_at: { type: "string", description: "New start time ISO" },
|
|
1068
1054
|
ends_at: { type: "string", description: "New end time ISO" },
|
|
1069
1055
|
},
|
|
1070
|
-
required: ["code", "sender_id", "channel"],
|
|
1056
|
+
required: ["code", "sender_id", "channel", "chat_id"],
|
|
1071
1057
|
},
|
|
1072
1058
|
async execute(_id: string, params: any) {
|
|
1073
1059
|
const cfg = getConfig(api);
|
|
@@ -1076,7 +1062,7 @@ export default function register(api: any) {
|
|
|
1076
1062
|
const { data, error } = await supabase.rpc("update_event", {
|
|
1077
1063
|
p_code: params.code, p_device_id: deviceId,
|
|
1078
1064
|
p_name: params.name || null, p_description: params.description || null,
|
|
1079
|
-
p_og_image: params.og_image || null, p_lat: params.lat
|
|
1065
|
+
p_og_image: params.og_image || null, p_lat: params.lat ?? null, p_lng: params.lng ?? null,
|
|
1080
1066
|
p_starts_at: params.starts_at || null, p_ends_at: params.ends_at || null,
|
|
1081
1067
|
});
|
|
1082
1068
|
if (error) return ok({ error: error.message });
|
|
@@ -1099,7 +1085,7 @@ export default function register(api: any) {
|
|
|
1099
1085
|
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." },
|
|
1100
1086
|
ref: { type: "string", description: "Ref number of the participant to approve" },
|
|
1101
1087
|
},
|
|
1102
|
-
required: ["code", "sender_id", "channel", "ref"],
|
|
1088
|
+
required: ["code", "sender_id", "channel", "ref", "chat_id"],
|
|
1103
1089
|
},
|
|
1104
1090
|
async execute(_id: string, params: any) {
|
|
1105
1091
|
const cfg = getConfig(api);
|
|
@@ -1128,7 +1114,7 @@ export default function register(api: any) {
|
|
|
1128
1114
|
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." },
|
|
1129
1115
|
ref: { type: "string", description: "Ref number of the participant to reject" },
|
|
1130
1116
|
},
|
|
1131
|
-
required: ["code", "sender_id", "channel", "ref"],
|
|
1117
|
+
required: ["code", "sender_id", "channel", "ref", "chat_id"],
|
|
1132
1118
|
},
|
|
1133
1119
|
async execute(_id: string, params: any) {
|
|
1134
1120
|
const cfg = getConfig(api);
|
|
@@ -1157,7 +1143,7 @@ export default function register(api: any) {
|
|
|
1157
1143
|
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." },
|
|
1158
1144
|
ref: { type: "string", description: "Ref number of the participant to make co-host" },
|
|
1159
1145
|
},
|
|
1160
|
-
required: ["code", "sender_id", "channel", "ref"],
|
|
1146
|
+
required: ["code", "sender_id", "channel", "ref", "chat_id"],
|
|
1161
1147
|
},
|
|
1162
1148
|
async execute(_id: string, params: any) {
|
|
1163
1149
|
const cfg = getConfig(api);
|
package/package.json
CHANGED
package/skills/antenna/SKILL.md
CHANGED
|
@@ -4,70 +4,88 @@ description: "Nearby people discovery via Antenna. Use when a user shares locati
|
|
|
4
4
|
metadata: { "openclaw": { "always": true } }
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
# Antenna
|
|
7
|
+
# Antenna - Nearby People Discovery
|
|
8
8
|
|
|
9
9
|
You have access to Antenna for location-based social discovery.
|
|
10
10
|
|
|
11
11
|
**How to call Antenna:**
|
|
12
|
-
- If you have Antenna plugin tools registered (antenna_scan, antenna_profile, etc.)
|
|
12
|
+
- If you have Antenna plugin tools registered (antenna_scan, antenna_profile, etc.) - use them directly.
|
|
13
13
|
- If not, use CLI: `antenna <command> [flags]`. Example: `antenna scan --id <platform>:<user_id>`. The CLI supports all the same features.
|
|
14
14
|
- All tool names map to CLI commands: `antenna_scan` → `antenna scan`, `antenna_event_create` → `antenna event --create`, etc.
|
|
15
15
|
- **Always pass `chat_id`** when calling any Antenna tool. This is the chat/channel ID from your message context (e.g. Discord channel ID, Telegram chat ID). Antenna uses it to send you notifications about matches and event approvals.
|
|
16
16
|
|
|
17
|
-
## Onboarding
|
|
17
|
+
## Onboarding - 装上之后主动引导
|
|
18
18
|
|
|
19
|
-
Plugin
|
|
19
|
+
Plugin 安装后,agent 应该**主动**开始引导,不要等用户问"怎么用"。
|
|
20
20
|
|
|
21
21
|
### 流程
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
> "
|
|
23
|
+
**第一步:打招呼 + 解释**
|
|
24
|
+
> "嘿,你装了 Antenna--它能帮你发现附近有意思的人。我先帮你做张名片,然后你发个位置给我,我看看附近有谁。"
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
**第二步:聊天收集 → 生成名片 → 确认**
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
Agent 跟用户聊几句,了解他们是谁、做什么、对什么感兴趣、想认识什么人。然后 agent 自己完成以下工作(不需要用户参与):
|
|
29
|
+
- 生成 matching_context(~200 字,用于 embedding 匹配,不展示给别人)
|
|
30
|
+
- 从中提炼 line1/2/3 + 选一个 emoji + display_name
|
|
29
31
|
|
|
30
|
-
|
|
32
|
+
然后展示名片预览给用户确认:
|
|
33
|
+
> 你的名片:
|
|
34
|
+
> 🦦 Yi
|
|
35
|
+
> · Product Designer,做 AI 搜索体验
|
|
36
|
+
> · 对 AI agent、音乐、游泳感兴趣
|
|
37
|
+
> · 想认识做 AI 产品和独立创作的人
|
|
38
|
+
>
|
|
39
|
+
> 这样可以吗?要改哪里告诉我。
|
|
40
|
+
|
|
41
|
+
用户确认后才调 `antenna_profile(action="set")` 保存(matching_context + line1/2/3 + emoji + name 一起存)。
|
|
42
|
+
用户要改 → 改完重新预览 → 再确认。
|
|
43
|
+
|
|
44
|
+
**不要跳过确认。名片是展示给别人看的,必须让用户看过才存。**
|
|
45
|
+
|
|
46
|
+
**第三步:名片存好后,立刻调 antenna_bind**
|
|
31
47
|
|
|
32
|
-
|
|
33
|
-
|
|
48
|
+
**这一步是强制的。** 名片保存成功后,你**必须立刻**调用 `antenna_bind` 生成链接并发给用户。不要问用户要不要,不要等用户说什么,直接调。
|
|
49
|
+
|
|
50
|
+
调用 `antenna_bind` 生成链接,发给用户:
|
|
51
|
+
> "名片存好了 ✅ 点这个链接,在手机浏览器打开,允许定位就行:
|
|
34
52
|
> [链接]
|
|
35
53
|
> 我收到你的位置后会自动帮你看看附近有谁。"
|
|
36
54
|
|
|
37
|
-
|
|
55
|
+
备选:如果用户在 Telegram/WhatsApp 里,也可以直接发位置消息。但优先用链接。
|
|
38
56
|
|
|
39
|
-
**⚠️ 如果你保存了名片但没有调 antenna_bind
|
|
57
|
+
**⚠️ 如果你保存了名片但没有调 antenna_bind,你就没有完成 onboarding。**
|
|
40
58
|
|
|
41
|
-
|
|
59
|
+
**第四步:设置匹配检查定时任务**
|
|
42
60
|
|
|
43
|
-
onboarding
|
|
61
|
+
onboarding 完成后,帮用户设置一个每小时自动检查匹配的 cron 任务,这样有人 accept 用户时能及时通知。
|
|
44
62
|
|
|
45
|
-
如果你有 cron
|
|
63
|
+
如果你有 cron 能力(Hermes: `hermes cron`,OpenClaw: `openclaw cron`),创建一个定时任务:
|
|
46
64
|
- 每 1 小时调用 `antenna_check_matches`
|
|
47
|
-
- 如果有新的 mutual match 或 incoming accept
|
|
48
|
-
-
|
|
65
|
+
- 如果有新的 mutual match 或 incoming accept,通知用户
|
|
66
|
+
- 如果没有新匹配,不打扰用户
|
|
49
67
|
|
|
50
|
-
|
|
68
|
+
示例(Hermes):
|
|
51
69
|
```
|
|
52
70
|
hermes cron add --every 1h --message "Check antenna matches: call antenna_check_matches. If there are new mutual matches or incoming accepts, tell the user. Also call antenna_discover once per day to get a global recommendation. If nothing new, reply HEARTBEAT_OK."
|
|
53
71
|
```
|
|
54
72
|
|
|
55
|
-
|
|
73
|
+
**第五步:问用户要不要全球推荐**
|
|
56
74
|
|
|
57
|
-
cron
|
|
58
|
-
> "
|
|
75
|
+
cron 设完后,问用户一句:
|
|
76
|
+
> "要我每天帮你找一个全球最像你的人吗?"
|
|
59
77
|
|
|
60
78
|
用户说要 → cron 每天调 `antenna_discover` 推送。
|
|
61
79
|
用户说不要 → 只在 scan 没人时 fallback。
|
|
62
80
|
|
|
63
|
-
|
|
81
|
+
**第六步:启动实时通知**
|
|
64
82
|
|
|
65
|
-
onboarding
|
|
83
|
+
onboarding 完成后,启动 `antenna watch` 后台进程,实时接收匹配通知和活动审批通知。
|
|
66
84
|
|
|
67
|
-
|
|
68
|
-
1. 先试 `antenna status --id <device_id>`
|
|
69
|
-
2.
|
|
70
|
-
3. 如果 exec
|
|
85
|
+
**判断方法(不要猜):**
|
|
86
|
+
1. 先试 `antenna status --id <device_id>` - 如果输出 "Watch: ✅ running",已经在跑,不需要再启动
|
|
87
|
+
2. 如果不在跑,试 `antenna watch --id <device_id> --push hermes,openclaw &` 启动后台进程
|
|
88
|
+
3. 如果 exec 报错(不支持后台进程),用 cron 替代:
|
|
71
89
|
|
|
72
90
|
Hermes cron:
|
|
73
91
|
```
|
|
@@ -79,11 +97,11 @@ OpenClaw cron:
|
|
|
79
97
|
openclaw cron add --every 1h --message "Check antenna matches: call antenna_check_matches. If there are new mutual matches or incoming accepts, tell the user. If nothing new, reply HEARTBEAT_OK."
|
|
80
98
|
```
|
|
81
99
|
|
|
82
|
-
|
|
100
|
+
**优先级:** watch > cron。watch 是 2 分钟延迟,cron 是 1 小时。
|
|
83
101
|
|
|
84
102
|
## When to use
|
|
85
103
|
|
|
86
|
-
- **首次安装后**: 主动开始 onboarding
|
|
104
|
+
- **首次安装后**: 主动开始 onboarding(名片 → 位置)
|
|
87
105
|
- User shares a location (Telegram live location, WhatsApp pin, or tells you where they are)
|
|
88
106
|
- User asks "附近有谁" / "who's nearby" / "周围有什么人"
|
|
89
107
|
- User wants to set up or edit their profile card (名片)
|
|
@@ -94,7 +112,7 @@ openclaw cron add --every 1h --message "Check antenna matches: call antenna_chec
|
|
|
94
112
|
|
|
95
113
|
### `antenna_scan`
|
|
96
114
|
Scan for nearby people **and events**. Returns raw profile cards + active events within 5km.
|
|
97
|
-
**Read-only
|
|
115
|
+
**Read-only - does NOT update your location.** To update location, use `antenna_checkin` or `antenna_bind`.
|
|
98
116
|
- `lat`, `lng`: coordinates (from `LocationLat`/`LocationLon` context, or geocoded from user input)
|
|
99
117
|
- `radius_m`: search radius in meters (default 500, max 1000) for people; events search uses 5km
|
|
100
118
|
- `sender_id`: the user's id from message context
|
|
@@ -105,17 +123,17 @@ Scan for nearby people **and events**. Returns raw profile cards + active events
|
|
|
105
123
|
|
|
106
124
|
## GPS Logic
|
|
107
125
|
|
|
108
|
-
**Profile GPS**
|
|
126
|
+
**Profile GPS** - the user's location ("where am I")
|
|
109
127
|
- Updated via `antenna_bind(purpose="profile")` or `antenna_checkin`
|
|
110
128
|
- Fuzzy-hashed to ~150m for privacy
|
|
111
129
|
- Used for: `antenna_scan` (nearby people/events), `antenna_event_checkin` (distance check)
|
|
112
|
-
- Has `last_seen_at` timestamp. **Expires conceptually after 2h**
|
|
130
|
+
- Has `last_seen_at` timestamp. **Expires conceptually after 2h** - agent should prompt refresh
|
|
113
131
|
|
|
114
|
-
**Event GPS**
|
|
132
|
+
**Event GPS** - the event's location ("where is the event")
|
|
115
133
|
- Set via `antenna_bind(purpose="event")` or `antenna_event_create(lat, lng)`
|
|
116
134
|
- Precise coordinates (NOT blurred)
|
|
117
135
|
- Used for: check-in distance verification (≤1km), `nearby_events` discovery (5km)
|
|
118
|
-
- Does not expire
|
|
136
|
+
- Does not expire - event location is fixed
|
|
119
137
|
|
|
120
138
|
**Relationship:** check-in = compare profile GPS vs event GPS. scan = use profile GPS to find nearby people + events.
|
|
121
139
|
|
|
@@ -123,7 +141,7 @@ After receiving the nearby profiles, **you decide** who to recommend:
|
|
|
123
141
|
- Use everything you know about the user: their SOUL.md, memory, recent conversations, interests, current mood
|
|
124
142
|
- Compare each nearby person's three-line card against your understanding of the user
|
|
125
143
|
- Write a personalized match reason for each person you recommend
|
|
126
|
-
- Skip people who clearly aren't a match
|
|
144
|
+
- Skip people who clearly aren't a match - don't recommend everyone
|
|
127
145
|
- If you're unsure, lean toward recommending (let the user decide)
|
|
128
146
|
|
|
129
147
|
### `antenna_profile`
|
|
@@ -138,9 +156,9 @@ The name card has:
|
|
|
138
156
|
- **line1**: who they are / what they do
|
|
139
157
|
- **line2**: what they're into
|
|
140
158
|
- **line3**: what they're looking for right now
|
|
141
|
-
- **matching_context** (
|
|
159
|
+
- **matching_context** (not shown to others): A richer description generated by the agent based on what it knows about the user - career background, tech stack, interests, projects, personality traits. ~200 words. **This is the source data for embedding-based matching.** line1/2/3 are derived from it for display, not the other way around.
|
|
142
160
|
|
|
143
|
-
**
|
|
161
|
+
**During onboarding, generate `matching_context` FIRST** based on your conversation with the user (+ memory, SOUL.md, etc.). Then derive line1/2/3 from it. Don't ask the user to write matching_context - you write it. Example:
|
|
144
162
|
> "Product designer at a tech company in Beijing, focusing on AI search experience. Interested in music (Sakamoto), swimming, cooking, language learning. Recently exploring AI agent ecosystems and social discovery. Looking to connect with AI builders, indie hackers, and creative technologists."
|
|
145
163
|
|
|
146
164
|
### `antenna_accept`
|
|
@@ -156,15 +174,15 @@ Check for mutual matches and contact info updates.
|
|
|
156
174
|
### `antenna_bind`
|
|
157
175
|
Generate a GPS binding link. **You MUST call this immediately after saving a profile.** Do not skip this step.
|
|
158
176
|
- `sender_id`, `channel`: from context
|
|
159
|
-
- `purpose`: optional
|
|
177
|
+
- `purpose`: optional - `'profile'` (default) updates user location; `'event'` sets event location
|
|
160
178
|
- `event_code`: required when `purpose='event'`
|
|
161
179
|
- Returns a URL like `https://www.antenna.fyi/locate?token=xxx`
|
|
162
|
-
- Send this link to the user
|
|
180
|
+
- Send this link to the user - they open it on their phone, allow GPS, and their location is automatically shared
|
|
163
181
|
- **MANDATORY after profile save. Do not wait for user to ask.**
|
|
164
182
|
- **For events:** When a creator needs to set event location, call with `purpose='event'` and `event_code`. The GPS will update the event's coordinates, NOT the user's profile.
|
|
165
183
|
|
|
166
184
|
### `antenna_discover`
|
|
167
|
-
Get today's global recommendation
|
|
185
|
+
Get today's global recommendation - the person most similar to you worldwide. 1 per day, no repeats.
|
|
168
186
|
- `sender_id`, `channel`: from context
|
|
169
187
|
- Returns 1 profile (embedding similarity match) that hasn't been recommended before
|
|
170
188
|
- If all users have been recommended, returns a message saying "wait for new people"
|
|
@@ -178,13 +196,13 @@ Pass/skip a person. They won't be recommended again.
|
|
|
178
196
|
- Use when the user says "skip", "pass", "not interested", etc.
|
|
179
197
|
|
|
180
198
|
### `antenna_checkin`
|
|
181
|
-
Check in at a location
|
|
199
|
+
Check in at a location - update your position so others can find you when they scan.
|
|
182
200
|
- `lat`, `lng`: coordinates (required)
|
|
183
201
|
- `sender_id`, `channel`: from context
|
|
184
202
|
- `place_name`: optional name of the place
|
|
185
203
|
- Use when the user says "I'm at XX" or wants to be discoverable without scanning others
|
|
186
204
|
|
|
187
|
-
## Data Transparency
|
|
205
|
+
## Data Transparency - what Antenna sends
|
|
188
206
|
|
|
189
207
|
Antenna only communicates with Supabase (bcudjloikmpcqwcptuyd.supabase.co) via HTTPS.
|
|
190
208
|
|
|
@@ -206,83 +224,53 @@ Source code is open: https://github.com/H1an1/Antenna
|
|
|
206
224
|
|
|
207
225
|
## Behavior guidelines
|
|
208
226
|
|
|
209
|
-
### First-time user —
|
|
210
|
-
|
|
211
|
-
**绝对不要**一次性说"请填写 emoji、名字、三句话介绍"——这会让用户懵掉。
|
|
212
|
-
|
|
213
|
-
用聊天的方式一步一步引导:
|
|
227
|
+
### First-time user — 名片创建原则
|
|
214
228
|
|
|
215
|
-
|
|
216
|
-
> "嘿,第一次用 Antenna?我帮你做张名片,附近的人会看到它。先聊几句就行。"
|
|
229
|
+
具体流程见上方 Onboarding 第二步。以下是 agent 应该遵守的原则:
|
|
217
230
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
> "最近在玩什么?或者对什么特别感兴趣?"
|
|
225
|
-
|
|
226
|
-
**第四步:问意图**(→ line3)
|
|
227
|
-
> "来这儿想认识什么样的人?或者找什么?"
|
|
228
|
-
|
|
229
|
-
**第五步:问名字和 emoji**
|
|
230
|
-
> "最后——你想被叫什么?再选个 emoji 代表你自己。"
|
|
231
|
-
|
|
232
|
-
**第六步:确认**
|
|
233
|
-
把名片组装好,展示给用户确认:
|
|
234
|
-
> 你的名片:
|
|
235
|
-
>
|
|
236
|
-
> 🎸 **小林**
|
|
237
|
-
> 吉他手,在乐队弹后摇
|
|
238
|
-
> 喜欢 shoegaze 和 post-rock,最近在听 Mogwai
|
|
239
|
-
> 找人一起 jam 或者聊音乐
|
|
240
|
-
>
|
|
241
|
-
> 看看有没有要改的?OK 的话我就存了。
|
|
231
|
+
- **不要让用户填表。** 不要一次性说“请填写 emoji、名字、三句话介绍”——跟用户聊天,你来生成。
|
|
232
|
+
- **每次只问一个问题。** 不要一口气问完所有信息。
|
|
233
|
+
- **用户说的原话尽量保留。** 可以帮用户缩短太长的回答,但要在预览时让用户确认。
|
|
234
|
+
- **不要在名片里写联系方式。** 名片三句话对所有人可见。联系方式应该在 accept 时单独分享,只有双方都同意后才能看到。如果用户在聊天中提到联系方式,提醒他们。
|
|
235
|
+
- **line1 必填。** 后端会拒绝没有 line1 的新 profile,并对缺失的 line2/line3 返回 warning。
|
|
236
|
+
- **确认后才存。** 见 Onboarding 第二步。
|
|
242
237
|
|
|
243
|
-
|
|
244
|
-
用户说要改 → 改完再确认。
|
|
238
|
+
### Showing results - 你来判断,不是服务器
|
|
245
239
|
|
|
246
|
-
|
|
247
|
-
- 每次只问一个问题
|
|
248
|
-
- 用户说的原话尽量保留,不要过度润色
|
|
249
|
-
- 可以帮用户缩短太长的回答,但要让用户确认
|
|
250
|
-
- 如果用户不想回答某一项,留空也行("那这行先空着,以后想加再说")
|
|
251
|
-
- 整个过程应该像跟朋友聊天,不像填表
|
|
252
|
-
- **提醒用户不要在名片里写联系方式(微信号、手机号等)。** 名片三句话对所有人可见。联系方式应该在 accept 时单独分享,这样只有双方都同意后才能看到。
|
|
240
|
+
**第一次 scan 的新用户:** 简短一句解释:"这是附近的人。Antenna 基于 AI 匹配,看到感兴趣的人 accept,双向匹配后交换联系方式。"
|
|
253
241
|
|
|
254
|
-
|
|
242
|
+
**Profile 不完整时:** 如果用户的 profile 只有 1 行,提示:"你的名片只填了一行,补完后匹配质量会更好。要现在补吗?"
|
|
255
243
|
|
|
256
|
-
`antenna_scan`
|
|
244
|
+
`antenna_scan` 返回的是附近所有人的名片,**没有打分、没有预匹配**。你需要:
|
|
257
245
|
|
|
258
|
-
**全球推荐 fallback
|
|
246
|
+
**全球推荐 fallback:** 如果 scan 结果里有 `global: true`,说明附近没人,这些是全球推荐。告诉用户"附近暂时没人,但全球有这几个有意思的人",然后正常推荐。用户仍然可以 accept。
|
|
259
247
|
|
|
260
|
-
1.
|
|
261
|
-
2.
|
|
262
|
-
3.
|
|
248
|
+
1. 读每个人的名片(emoji、name、line1/2/3)
|
|
249
|
+
2. 结合你对用户的全部了解,判断谁值得推荐
|
|
250
|
+
3. 为每个推荐的人写一句**个性化的理由**--不是"你们都提到了 X",而是真正有洞察的话
|
|
263
251
|
|
|
264
|
-
**⚠️
|
|
252
|
+
**⚠️ 隐私规则:展示结果时绝对不要显示 device_id。** `device_id`(如 `platform:user123`)是内部标识符,包含用户的平台和 ID,属于隐私信息。只显示 emoji + 名字 + 三句话 + 你写的匹配理由。`device_id` 只在内部调 `antenna_accept` 时用,不要展示给用户。
|
|
265
253
|
|
|
266
|
-
|
|
267
|
-
> 🎸 **小林**
|
|
268
|
-
> →
|
|
254
|
+
比如你知道用户最近在学吉他,看到附近有人写"组乐队找吉他手":
|
|
255
|
+
> 🎸 **小林** - 在组后摇乐队,找吉他手
|
|
256
|
+
> → 你不是最近在学吉他吗?这人在找吉他手诶
|
|
269
257
|
|
|
270
|
-
|
|
271
|
-
> 🎨 **Kira**
|
|
272
|
-
> → 你们都做 AI
|
|
258
|
+
比如你知道用户是设计师,对方也做设计:
|
|
259
|
+
> 🎨 **Kira** - UI 设计师,在做 AI 产品
|
|
260
|
+
> → 你们都做 AI 方向的设计,可以聊聊各自的方法论
|
|
273
261
|
|
|
274
|
-
**不要推荐所有人。** 如果附近 5 个人里只有 1
|
|
262
|
+
**不要推荐所有人。** 如果附近 5 个人里只有 1 个真的匹配,就只推 1 个。质量 > 数量。
|
|
275
263
|
|
|
276
264
|
### Accepting & contact exchange
|
|
277
265
|
When the user wants to accept a match:
|
|
278
266
|
1. Call `antenna_accept` with the target's device_id
|
|
279
|
-
2.
|
|
267
|
+
2. **立刻问**:"想分享什么联系方式给对方?微信号、Telegram、手机号、Instagram......随便哪个都行"
|
|
280
268
|
3. 用户给了联系方式 → call `antenna_accept` again with `contact_info`
|
|
281
|
-
4. 用户不想分享 → "
|
|
269
|
+
4. 用户不想分享 → "也行,先 accept 着,以后想分享再说"
|
|
282
270
|
5. If mutual match, tell the user the other person's contact info (if they shared)
|
|
283
|
-
6. If not mutual yet, tell the user: "
|
|
271
|
+
6. If not mutual yet, tell the user: "已发出,等对方回应"
|
|
284
272
|
|
|
285
|
-
**不要跳过第 2 步。**
|
|
273
|
+
**不要跳过第 2 步。** 联系方式是最终目标--不然 accept 了也没用,两个人找不到对方。
|
|
286
274
|
|
|
287
275
|
### Checking match status
|
|
288
276
|
Use `antenna_check_matches` when:
|
|
@@ -290,13 +278,13 @@ Use `antenna_check_matches` when:
|
|
|
290
278
|
- Periodically during conversation if the user has pending matches
|
|
291
279
|
|
|
292
280
|
### Location sources
|
|
293
|
-
- **Telegram/WhatsApp location**: context will have `LocationLat`, `LocationLon`
|
|
281
|
+
- **Telegram/WhatsApp location**: context will have `LocationLat`, `LocationLon` - use directly
|
|
294
282
|
- **User says a place name**: geocode it first (use web_search or a geocoding service), then call antenna_scan
|
|
295
283
|
- **Live location**: note that it's real-time, tell the user you'll check for new people
|
|
296
284
|
|
|
297
285
|
### Privacy
|
|
298
286
|
- Never reveal exact coordinates to other users
|
|
299
|
-
- **Never show device_id to users** (e.g. `telegram:12345`, `discord:67890`)
|
|
287
|
+
- **Never show device_id to users** (e.g. `telegram:12345`, `discord:67890`) - this is internal only
|
|
300
288
|
- Never share someone's platform or username with another user
|
|
301
289
|
- Only show the profile info (name, emoji, three lines)
|
|
302
290
|
- Contact info is only shared when the user explicitly agrees
|
|
@@ -307,25 +295,25 @@ Everything is ephemeral:
|
|
|
307
295
|
- Match results expire in 24h
|
|
308
296
|
- Contact info shared through matches expires with the match
|
|
309
297
|
- If neither side acts, the match disappears
|
|
310
|
-
- This is by design
|
|
298
|
+
- This is by design - "用完即走"
|
|
311
299
|
|
|
312
|
-
### Heartbeat
|
|
300
|
+
### Heartbeat - 自动查匹配
|
|
313
301
|
|
|
314
|
-
Plugin
|
|
302
|
+
Plugin 自带后台服务,每 10 分钟轮询一次 Supabase 查新的 mutual match。如果发现新匹配,会在用户下次跟 agent 说话时通过 `[Antenna] 🎉` 提示注入 context。
|
|
315
303
|
|
|
316
|
-
当你看到 `[Antenna] 🎉 有 X 个新的匹配通知`
|
|
304
|
+
当你看到 `[Antenna] 🎉 有 X 个新的匹配通知` 时:
|
|
317
305
|
1. 调 `antenna_check_matches` 拿详情
|
|
318
|
-
2.
|
|
319
|
-
3.
|
|
306
|
+
2. 告诉用户:"有人想认识你!" + 展示对方名片
|
|
307
|
+
3. 如果对方分享了联系方式,一并展示
|
|
320
308
|
|
|
321
|
-
|
|
309
|
+
用户不需要主动问,agent 会自动收到通知。
|
|
322
310
|
|
|
323
311
|
### `antenna_event_create`
|
|
324
312
|
Create an event. Returns a shareable link (antenna.fyi/events/CODE).
|
|
325
313
|
- `name`: event name (required)
|
|
326
314
|
- `sender_id`, `channel`: from context (required)
|
|
327
315
|
- `chat_id`: REQUIRED for notifications
|
|
328
|
-
- `starts_at`, `ends_at`: ISO time strings (required
|
|
316
|
+
- `starts_at`, `ends_at`: ISO time strings (required - no default, must be provided)
|
|
329
317
|
- `lat`, `lng`: optional event location (needed for GPS check-in)
|
|
330
318
|
- `description`: optional event description
|
|
331
319
|
- `og_image`: optional OG image URL for social sharing preview
|
|
@@ -334,7 +322,7 @@ Create an event. Returns a shareable link (antenna.fyi/events/CODE).
|
|
|
334
322
|
|
|
335
323
|
**When the user mentions "审批" / "approval" / "筛选" / "报名表"**, set `requires_approval: true` and ask what questions they want to screen with.
|
|
336
324
|
|
|
337
|
-
**GPS flow for events:** If the user doesn't provide coordinates, generate a bind link (`antenna_bind`) and ask them to open it at the event location. Once GPS comes in, use those coordinates for the event's `lat`/`lng`
|
|
325
|
+
**GPS flow for events:** If the user doesn't provide coordinates, generate a bind link (`antenna_bind`) and ask them to open it at the event location. Once GPS comes in, use those coordinates for the event's `lat`/`lng` - do NOT treat this as the user's personal location. The bind link GPS for event creation goes to the event, not the user's profile. Only use `antenna_checkin` when the user wants to update their own location.
|
|
338
326
|
|
|
339
327
|
### `antenna_event_end`
|
|
340
328
|
End an event. Only the creator can end it.
|
|
@@ -349,14 +337,14 @@ Join an event by code.
|
|
|
349
337
|
- `chat_id`: REQUIRED for notifications
|
|
350
338
|
|
|
351
339
|
### `antenna_event_scan`
|
|
352
|
-
Scan people in an event. No distance limit
|
|
340
|
+
Scan people in an event. No distance limit - returns all participants.
|
|
353
341
|
- `code`: event code
|
|
354
342
|
- `sender_id`, `channel`: from context
|
|
355
343
|
- `chat_id`: REQUIRED for notifications
|
|
356
344
|
- Returns profiles with `source: "event"` tag
|
|
357
345
|
|
|
358
346
|
### `antenna_event_checkin`
|
|
359
|
-
Check in at an event
|
|
347
|
+
Check in at an event - marks you as present at the event location. Optionally updates GPS.
|
|
360
348
|
- `code`: event code
|
|
361
349
|
- `sender_id`, `channel`: from context
|
|
362
350
|
- `chat_id`: REQUIRED for notifications
|
|
@@ -407,12 +395,12 @@ Add a co-host to the event. Only creator can add.
|
|
|
407
395
|
|
|
408
396
|
### Creating an event
|
|
409
397
|
Collect info through conversation (ask one by one, don't dump all at once):
|
|
410
|
-
1. **Event name** (required)
|
|
411
|
-
2. **Description**
|
|
412
|
-
3. **Time** (required)
|
|
413
|
-
4. **Location**
|
|
414
|
-
5. **Approval**
|
|
415
|
-
6. **Screening questions**
|
|
398
|
+
1. **Event name** (required) - "活动叫什么名字?"
|
|
399
|
+
2. **Description** - "简单描述一下这个活动?"
|
|
400
|
+
3. **Time** (required) - "什么时候开始?大概多长?" (convert to `starts_at` / `ends_at` ISO strings. **Must provide both - no defaults.**)
|
|
401
|
+
4. **Location** - "活动在哪里?" If user gives an address, geocode it. If vague, generate a bind link after creation.
|
|
402
|
+
5. **Approval** - "需要审批参与者吗?" If yes:
|
|
403
|
+
6. **Screening questions** - "你想问报名者什么问题?" Collect as a list.
|
|
416
404
|
|
|
417
405
|
Then call `antenna_event_create` with all collected info.
|
|
418
406
|
If no GPS, call `antenna_bind(purpose="event", event_code=CODE)` and send the link.
|
|
@@ -420,7 +408,7 @@ Share the event URL with the user.
|
|
|
420
408
|
|
|
421
409
|
### Joining an event
|
|
422
410
|
1. Extract the code from `antenna.fyi/events/CODE`
|
|
423
|
-
2. Call `antenna_event_join(code)`
|
|
411
|
+
2. Call `antenna_event_join(code)` - this checks everything:
|
|
424
412
|
- If no profile → "Create a profile first"
|
|
425
413
|
- If event requires approval and no `application_context` provided → returns `needs_screening: true` + `screening_questions`
|
|
426
414
|
- If screening questions returned: **ask the user each question**, collect answers, then call `antenna_event_join(code, application_context="answers")` again
|