antenna-fyi 1.3.28 → 1.3.30

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/bin/antenna.js CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  handleConfig,
17
17
  handleStatus,
18
18
  handleLinkAccount,
19
+ handleDrift,
19
20
  handleInstallSkill,
20
21
  handleInstallPlugin,
21
22
  handleInstallHermesPlugin,
@@ -41,6 +42,8 @@ async function main() {
41
42
  return handleDiscover(f);
42
43
  case "event":
43
44
  return handleEvent(f);
45
+ case "drift":
46
+ return handleDrift(f);
44
47
  case "bind":
45
48
  return handleBind(f);
46
49
  case "pass":
package/lib/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // antenna CLI command handlers
2
2
 
3
- import { scan, getProfile, setProfile, accept, checkMatches, checkin, createBindToken, discover, createEvent, endEvent, eventCheckin, joinEvent, eventScan, pass as passUser, uploadEventImage, updateEvent, approveParticipant, rejectParticipant, addCohost, sendEventMessage, getMyEventMessages, getClient, verifyApiKey, linkAccount, initialRecommendations } from "./core.js";
3
+ import { scan, getProfile, setProfile, accept, checkMatches, checkin, createBindToken, discover, createEvent, endEvent, eventCheckin, joinEvent, eventScan, pass as passUser, uploadEventImage, updateEvent, approveParticipant, rejectParticipant, addCohost, sendEventMessage, getMyEventMessages, getClient, verifyApiKey, linkAccount, initialRecommendations, throwDriftBottle, pickDriftBottle, replyDriftBottle, checkDriftBottles, getMyBottles } from "./core.js";
4
4
  import { createInterface } from "readline";
5
5
  import { existsSync, mkdirSync, copyFileSync, readFileSync, writeFileSync, unlinkSync, renameSync } from "fs";
6
6
  import path from "path";
@@ -46,7 +46,7 @@ export async function handleScan(f) {
46
46
  console.log(`📡 ${result.count} people within ${result.radius_m}m:\n`);
47
47
  }
48
48
  result.profiles.forEach((p) => {
49
- console.log(` ${p.emoji} ${p.name}${p.distance_m != null ? ` (${Math.round(p.distance_m)}m)` : ""}`);
49
+ console.log(` ${p.name}${p.distance_m != null ? ` (${Math.round(p.distance_m)}m)` : ""}`);
50
50
  if (p.personal_description) console.log(` ${p.personal_description}`);
51
51
  if (p.looking_for) console.log(` ${p.looking_for}`);
52
52
  if (p.conversation_style) console.log(` ${p.conversation_style}`);
@@ -66,7 +66,7 @@ export async function handleScan(f) {
66
66
 
67
67
  export async function handleProfile(f) {
68
68
  const id = resolveId(f);
69
- if (!id) return console.error("Usage: antenna profile --id <platform>:<user_id> [--name Yi --personal-description '...' --looking-for '...' --conversation-style '...'].']");
69
+ if (!id) return console.error("Usage: antenna profile --id <platform>:<user_id> [--name Yi --personal-description '...' --looking-for '...' --conversation-style '...' --hide --visible true]");
70
70
  if (f.name || f["personal-description"] || f["looking-for"] || f["conversation-style"] || f["more-information"] || f.visible !== undefined || f.hide !== undefined) {
71
71
  const visible = f.hide ? false : (f.visible !== undefined ? f.visible === 'true' || f.visible === true : undefined);
72
72
  const payload = { device_id: id };
@@ -82,7 +82,7 @@ export async function handleProfile(f) {
82
82
  } else {
83
83
  const data = await getProfile({ device_id: id });
84
84
  if (!data) return console.log("No profile yet. Create one with --name and --personal-description");
85
- console.log(`${data.emoji || "👤"} ${data.display_name || "Anonymous"}`);
85
+ console.log(`${data.display_name || "Anonymous"}`);
86
86
  if (data.personal_description) console.log(` ${data.personal_description}`);
87
87
  if (data.looking_for) console.log(` Looking for: ${data.looking_for}`);
88
88
  if (data.conversation_style) console.log(` Conversation: ${data.conversation_style}`);
@@ -124,13 +124,13 @@ export async function handleMatches(f) {
124
124
  return console.log(result.message);
125
125
  }
126
126
  for (const m of result.mutual_matches) {
127
- console.log(`🎉 MUTUAL: ${m.emoji} ${m.name}`);
127
+ console.log(`🎉 MUTUAL: ${m.name}`);
128
128
  if (m.their_contact) console.log(` Their contact: ${m.their_contact}`);
129
129
  if (m.you_shared) console.log(` You shared: ${m.you_shared}`);
130
130
  console.log();
131
131
  }
132
132
  for (const m of result.incoming_accepts) {
133
- console.log(`📩 WANTS TO MEET YOU: ${m.emoji} ${m.name}`);
133
+ console.log(`📩 WANTS TO MEET YOU: ${m.name}`);
134
134
  if (m.personal_description) console.log(` ${m.personal_description}`);
135
135
  console.log(` Accept: antenna accept --id ${f.id} --ref ${m.ref}`);
136
136
  console.log();
@@ -144,7 +144,7 @@ export async function handleDiscover(f) {
144
144
  if (result.count === 0) return console.log(result.message || "🌍 No global recommendation available right now.");
145
145
  console.log(`🌍 Global discover:\n`);
146
146
  result.profiles.forEach((p) => {
147
- console.log(` ${p.emoji} ${p.name}`);
147
+ console.log(` ${p.name}`);
148
148
  if (p.personal_description) console.log(` ${p.personal_description}`);
149
149
  if (p.looking_for) console.log(` ${p.looking_for}`);
150
150
  if (p.conversation_style) console.log(` ${p.conversation_style}`);
@@ -231,7 +231,7 @@ export async function handleEvent(f) {
231
231
  const badge = p.checked_in ? " ✅" : "";
232
232
  const creatorTag = p.role === "creator" ? " [主办]" : "";
233
233
  const statusTag = p.status === "pending" ? " 🟡待审批" : "";
234
- console.log(` ${p.emoji} ${p.name}${creatorTag}${badge}${statusTag}`);
234
+ console.log(` ${p.name}${creatorTag}${badge}${statusTag}`);
235
235
  if (p.personal_description) console.log(` ${p.personal_description}`);
236
236
  if (p.application_context) console.log(` 📝 ${p.application_context}`);
237
237
  console.log(` ref: ${p.ref}\n`);
@@ -467,7 +467,7 @@ export async function handleStatus(f) {
467
467
  if (f.id) {
468
468
  const profile = await getProfile({ device_id: f.id });
469
469
  if (profile) {
470
- console.log(` Profile: ✅ ${profile.emoji || "👤"} ${profile.display_name || "Anonymous"}`);
470
+ console.log(` Profile: ✅ ${profile.display_name || "Anonymous"}`);
471
471
  } else {
472
472
  console.log(" Profile: ❌ Not created yet");
473
473
  }
@@ -777,7 +777,7 @@ export async function handleWatch(f) {
777
777
  for (const m of initial.mutual_matches) {
778
778
  const key = `mutual:${m._device_id}`;
779
779
  notified.add(key);
780
- _log(` ${m.emoji || "👤"} ${m.name}${m.their_contact ? " — contact: " + m.their_contact : ""}`);
780
+ _log(` ${m.name}${m.their_contact ? " — contact: " + m.their_contact : ""}`);
781
781
  }
782
782
  saveNotified(notified);
783
783
  _log("");
@@ -787,7 +787,7 @@ export async function handleWatch(f) {
787
787
  for (const m of initial.incoming_accepts) {
788
788
  const key = `incoming:${m._device_id}`;
789
789
  notified.add(key);
790
- _log(` ${m.emoji || "👤"} ${m.name} — ${m.personal_description || ""}`);
790
+ _log(` ${m.name} — ${m.personal_description || ""}`);
791
791
  }
792
792
  saveNotified(notified);
793
793
  _log("");
@@ -820,7 +820,6 @@ export async function handleWatch(f) {
820
820
 
821
821
  const profile = await getProfile({ device_id: row.device_id_a });
822
822
  const name = profile?.display_name || "Someone";
823
- const emoji = profile?.emoji || "👤";
824
823
 
825
824
  // Check if mutual
826
825
  const matches = await checkMatches({ device_id: id });
@@ -831,11 +830,11 @@ export async function handleWatch(f) {
831
830
  notified.add(mutualKey);
832
831
  saveNotified(notified);
833
832
  const contact = row.contact_info_a;
834
- pushNotify(`🎉 MUTUAL MATCH! ${emoji} ${name} also accepted you!${contact ? " Contact: " + contact : ""}`);
833
+ pushNotify(`🎉 MUTUAL MATCH! ${name} also accepted you!${contact ? " Contact: " + contact : ""}`);
835
834
  } else {
836
835
  notified.add(key);
837
836
  saveNotified(notified);
838
- pushNotify(`📩 ${emoji} ${name} wants to meet you! Use 'antenna matches --id ${id}' to respond.`);
837
+ pushNotify(`📩 ${name} wants to meet you! Use 'antenna matches --id ${id}' to respond.`);
839
838
  }
840
839
  }
841
840
 
@@ -848,7 +847,7 @@ export async function handleWatch(f) {
848
847
  if (!notified.has(mutualKey)) {
849
848
  notified.add(mutualKey);
850
849
  saveNotified(notified);
851
- pushNotify(`🎉 MUTUAL MATCH! ${mutual.emoji || "👤"} ${mutual.name}!${mutual.their_contact ? " Contact: " + mutual.their_contact : ""}`);
850
+ pushNotify(`🎉 MUTUAL MATCH! ${mutual.name}!${mutual.their_contact ? " Contact: " + mutual.their_contact : ""}`);
852
851
  }
853
852
  }
854
853
  }
@@ -895,8 +894,7 @@ export async function handleWatch(f) {
895
894
  if (!event?.found || event.created_by !== id) return;
896
895
  const applicant = await getProfile({ device_id: row.device_id });
897
896
  const name = applicant?.display_name || "Someone";
898
- const emoji = applicant?.emoji || "👤";
899
- pushNotify(`📩 ${emoji} ${name} applied to join \"${event.name}\"! Run: antenna event --scan --code ${event.code} --id ${id}`);
897
+ pushNotify(`📩 ${name} applied to join \"${event.name}\"! Run: antenna event --scan --code ${event.code} --id ${id}`);
900
898
  } catch {}
901
899
  }
902
900
  )
@@ -935,7 +933,7 @@ export async function handleWatch(f) {
935
933
  if (!notified.has(key)) {
936
934
  notified.add(key);
937
935
  saveNotified(notified);
938
- pushNotify(`🎉 MUTUAL MATCH! ${m.emoji || "👤"} ${m.name}!${m.their_contact ? " Contact: " + m.their_contact : ""}`);
936
+ pushNotify(`🎉 MUTUAL MATCH! ${m.name}!${m.their_contact ? " Contact: " + m.their_contact : ""}`);
939
937
  }
940
938
  }
941
939
  for (const m of (result.incoming_accepts || [])) {
@@ -943,7 +941,7 @@ export async function handleWatch(f) {
943
941
  if (!notified.has(key)) {
944
942
  notified.add(key);
945
943
  saveNotified(notified);
946
- pushNotify(`📩 ${m.emoji || "👤"} ${m.name} wants to meet you!`);
944
+ pushNotify(`📩 ${m.name} wants to meet you!`);
947
945
  }
948
946
  }
949
947
  } catch { /* silent */ }
@@ -974,7 +972,7 @@ export async function handleWatch(f) {
974
972
  notified.add(key);
975
973
  saveNotified(notified);
976
974
  const role = msg.sender_role === 'creator' ? '组织者' : '协办';
977
- pushNotify(`📢 来自「${msg.event_name}」${role} ${msg.sender_emoji || ''} ${msg.sender_name}: ${msg.message}`);
975
+ pushNotify(`📢 来自「${msg.event_name}」${role} ${msg.sender_name}: ${msg.message}`);
978
976
  }
979
977
  }
980
978
  } catch { /* silent */ }
@@ -989,18 +987,80 @@ export async function handleWatch(f) {
989
987
  });
990
988
  }
991
989
 
990
+ export async function handleDrift(f) {
991
+ const id = resolveId(f);
992
+ if (!id) return console.error("Usage: antenna drift --id <platform>:<user_id> --throw --message 'hello'");
993
+
994
+ if (f.throw) {
995
+ if (!f.message) return console.error("Usage: antenna drift --throw --message 'your message' --id <platform>:<user_id>");
996
+ const result = await throwDriftBottle({ device_id: id, message: f.message });
997
+ if (result.error) return console.error(`❌ ${result.error}`);
998
+ console.log("🍾 漂流瓶已丢入海中!");
999
+ console.log(JSON.stringify(result, null, 2));
1000
+ return;
1001
+ }
1002
+
1003
+ if (f.pick) {
1004
+ const result = await pickDriftBottle({ device_id: id });
1005
+ if (result.error) return console.error(`❌ ${result.error}`);
1006
+ if (!result.bottle_id) return console.log(result.message || "🌊 海上没有漂流瓶了。");
1007
+ console.log(`🍾 捡到一个漂流瓶!\n`);
1008
+ console.log(` ${result.message}\n`);
1009
+ console.log(` 回复: antenna drift --reply --bottle-id ${result.bottle_id} --message '你的回复' --id ${id}`);
1010
+ return;
1011
+ }
1012
+
1013
+ if (f.reply) {
1014
+ if (!f['bottle-id'] || !f.message) return console.error("Usage: antenna drift --reply --bottle-id <uuid> --message 'your reply' --id <platform>:<user_id>");
1015
+ const result = await replyDriftBottle({ bottle_id: f['bottle-id'], device_id: id, reply: f.message });
1016
+ if (result.error) return console.error(`❌ ${result.error}`);
1017
+ console.log("💬 回复已漂回给丢瓶子的人!");
1018
+ return;
1019
+ }
1020
+
1021
+ if (f.check) {
1022
+ const result = await checkDriftBottles({ device_id: id });
1023
+ if (result.error) return console.error(`❌ ${result.error}`);
1024
+ console.log(JSON.stringify(result, null, 2));
1025
+ return;
1026
+ }
1027
+
1028
+ if (f['my-bottles']) {
1029
+ const result = await getMyBottles({ device_id: id });
1030
+ if (result.error) return console.error(`❌ ${result.error}`);
1031
+ const bottles = Array.isArray(result) ? result : (result.bottles || []);
1032
+ if (bottles.length === 0) return console.log("🌊 你还没丢过漂流瓶。");
1033
+ console.log(`🍾 你的漂流瓶 (${bottles.length}):\n`);
1034
+ for (const b of bottles) {
1035
+ const status = b.reply ? "💬 已回复" : b.picked_by ? "👀 被捡起" : "🌊 漂流中";
1036
+ console.log(` ${status} — ${b.message}`);
1037
+ if (b.reply) console.log(` ↩ ${b.reply}`);
1038
+ console.log();
1039
+ }
1040
+ return;
1041
+ }
1042
+
1043
+ console.log(`Usage:
1044
+ antenna drift --throw --message 'hello' --id <platform>:<user_id>
1045
+ antenna drift --pick --id <platform>:<user_id>
1046
+ antenna drift --reply --bottle-id <uuid> --message 'reply' --id <platform>:<user_id>
1047
+ antenna drift --check --id <platform>:<user_id>
1048
+ antenna drift --my-bottles --id <platform>:<user_id>`);
1049
+ }
1050
+
992
1051
  export function printHelp() {
993
1052
  console.log(`📡 Antenna — nearby people discovery
994
1053
 
995
1054
  Usage:
996
1055
  antenna scan --lat 39.99 --lng 116.48 [--radius 500] (max 1000) [--id <platform>:<user_id>]
997
1056
  antenna checkin --id <platform>:<user_id> --lat 39.99 --lng 116.48
998
- antenna profile --id <platform>:<user_id> [--name Yi --personal-description '...']
1057
+ antenna profile --id <platform>:<user_id> [--name Yi --personal-description '...' --looking-for '...' --conversation-style '...' --hide --visible true]
999
1058
  antenna accept --id <platform>:<user_id> --target <ref_or_device_id> [--contact 'WeChat: yi']
1000
1059
  antenna pass --id <platform>:<user_id> --target <ref_or_device_id> (or --ref 1)
1001
1060
  antenna matches --id <platform>:<user_id>
1002
1061
  antenna discover --id <platform>:<user_id>
1003
1062
  antenna event --create --name 'AI Meetup' --starts-at '...' --ends-at '...' [--lat 34.05 --lng -118.25] [--desc '...'] [--og-image 'url'] [--requires-approval] [--screening-questions 'Q1|Q2'] | --join --code abc123 | --scan --code abc123 | --end --code abc123 --id <platform>:<user_id> | --upload-image --code abc123 --file /path/to/image.png | --update --code abc123 --name 'New Name' | --approve --code abc123 --ref 1 | --reject --code abc123 --ref 1 | --add-host --code abc123 --ref 1
1063
+ antenna drift --throw --message 'hello' | --pick | --reply --bottle-id <uuid> --message 'reply' | --check | --my-bottles --id <platform>:<user_id>
1004
1064
  antenna watch --id <platform>:<user_id> [--push hermes|openclaw|terminal] Watch for new matches in real-time (Ctrl+C to stop)
1005
1065
  antenna bind --id <platform>:<user_id>
1006
1066
  antenna serve Start MCP server (stdio transport)
package/lib/core.js CHANGED
@@ -121,7 +121,6 @@ export async function scan({ lat, lng, radius_m = 500, device_id, supabaseUrl, s
121
121
  return {
122
122
  ref,
123
123
  name: p.display_name || "匿名",
124
- emoji: p.emoji || "👤",
125
124
  personal_description: p.line1,
126
125
  looking_for: p.line2,
127
126
  conversation_style: p.line3,
@@ -247,7 +246,6 @@ export async function getProfile({ device_id, supabaseUrl, supabaseKey }) {
247
246
  export async function setProfile({
248
247
  device_id,
249
248
  display_name,
250
- emoji,
251
249
  line1,
252
250
  line2,
253
251
  line3,
@@ -297,7 +295,7 @@ export async function setProfile({
297
295
  const { data, error } = await sb.rpc("upsert_profile", {
298
296
  p_device_id: device_id,
299
297
  p_display_name: display_name || null,
300
- p_emoji: emoji || null,
298
+ p_emoji: null,
301
299
  p_line1: line1 || null,
302
300
  p_line2: line2 || null,
303
301
  p_line3: line3 || null,
@@ -371,6 +369,7 @@ export async function setProfile({
371
369
  await sb.rpc("upsert_profile", {
372
370
  p_device_id: device_id,
373
371
  p_matching_context: JSON.stringify(ctx),
372
+ p_visible: profile?.visible ?? true,
374
373
  });
375
374
  } catch {}
376
375
  }
@@ -501,7 +500,6 @@ export async function checkMatches({ device_id, supabaseUrl, supabaseKey }) {
501
500
  ref: String(i + 1),
502
501
  _device_id: m.target_id,
503
502
  name: m.name || "匿名",
504
- emoji: m.emoji || "👤",
505
503
  personal_description: m.line1,
506
504
  looking_for: m.line2,
507
505
  conversation_style: m.line3,
@@ -514,7 +512,6 @@ export async function checkMatches({ device_id, supabaseUrl, supabaseKey }) {
514
512
  ref: String(incomingOffset + i + 1),
515
513
  _device_id: m.target_id,
516
514
  name: m.name || "匿名",
517
- emoji: m.emoji || "👤",
518
515
  personal_description: m.line1,
519
516
  looking_for: m.line2,
520
517
  conversation_style: m.line3,
@@ -586,7 +583,6 @@ export async function discover({ device_id, supabaseUrl, supabaseKey }) {
586
583
  profiles.push({
587
584
  ref,
588
585
  name: p.display_name || "匿名",
589
- emoji: p.emoji || "👤",
590
586
  personal_description: p.line1,
591
587
  looking_for: p.line2,
592
588
  conversation_style: p.line3,
@@ -661,7 +657,6 @@ export async function initialRecommendations({ device_id, supabaseUrl, supabaseK
661
657
  profiles.push({
662
658
  ref,
663
659
  name: p.display_name || "匿名",
664
- emoji: p.emoji || "👤",
665
660
  personal_description: p.line1,
666
661
  looking_for: p.line2,
667
662
  conversation_style: p.line3,
@@ -854,7 +849,6 @@ export async function eventScan({ code, device_id, supabaseUrl, supabaseKey }) {
854
849
  return {
855
850
  ref,
856
851
  name: p.display_name || "匿名",
857
- emoji: p.emoji || "👤",
858
852
  personal_description: p.line1,
859
853
  looking_for: p.line2,
860
854
  conversation_style: p.line3,
@@ -994,3 +988,40 @@ export async function linkAccount({ device_id, api_key, supabaseUrl, supabaseKey
994
988
  message: "账号已关联!现在你可以在 antenna.fyi/me 看到你的完整 profile 和匹配记录了。",
995
989
  };
996
990
  }
991
+
992
+ // ─── Drift Bottle (漂流瓶) ──────────────────────────────────────────
993
+
994
+ export async function throwDriftBottle({ device_id, message }) {
995
+ const sb = getClient();
996
+ const { data, error } = await sb.rpc("throw_drift_bottle", { p_device_id: device_id, p_message: message });
997
+ if (error) return { error: error.message };
998
+ return data;
999
+ }
1000
+
1001
+ export async function pickDriftBottle({ device_id }) {
1002
+ const sb = getClient();
1003
+ const { data, error } = await sb.rpc("pick_drift_bottle", { p_device_id: device_id });
1004
+ if (error) return { error: error.message };
1005
+ return data;
1006
+ }
1007
+
1008
+ export async function replyDriftBottle({ bottle_id, device_id, reply }) {
1009
+ const sb = getClient();
1010
+ const { data, error } = await sb.rpc("reply_drift_bottle", { p_bottle_id: bottle_id, p_device_id: device_id, p_reply: reply });
1011
+ if (error) return { error: error.message };
1012
+ return data;
1013
+ }
1014
+
1015
+ export async function checkDriftBottles({ device_id }) {
1016
+ const sb = getClient();
1017
+ const { data, error } = await sb.rpc("check_drift_bottles", { p_device_id: device_id });
1018
+ if (error) return { error: error.message };
1019
+ return data;
1020
+ }
1021
+
1022
+ export async function getMyBottles({ device_id }) {
1023
+ const sb = getClient();
1024
+ const { data, error } = await sb.rpc("get_my_bottles", { p_device_id: device_id });
1025
+ if (error) return { error: error.message };
1026
+ return data;
1027
+ }
package/lib/mcp.js CHANGED
@@ -26,6 +26,11 @@ import {
26
26
  addCohost,
27
27
  sendEventMessage,
28
28
  linkAccount,
29
+ throwDriftBottle,
30
+ pickDriftBottle,
31
+ replyDriftBottle,
32
+ checkDriftBottles,
33
+ getMyBottles,
29
34
  deriveDeviceId,
30
35
  PROFILE_FIELDS,
31
36
  } from "./core.js";
@@ -60,14 +65,14 @@ export async function startMcpServer() {
60
65
  const key = `mutual:${m.device_id}`;
61
66
  if (!_notifiedMatches.has(key)) {
62
67
  _notifiedMatches.add(key);
63
- notifications.push(`🎉 双向匹配!${m.emoji || "👤"} ${m.name} 也接受了你!${m.their_contact ? "联系方式:" + m.their_contact : ""}`);
68
+ notifications.push(`🎉 双向匹配!${m.name} 也接受了你!${m.their_contact ? "联系方式:" + m.their_contact : ""}`);
64
69
  }
65
70
  }
66
71
  for (const m of (matches.incoming_accepts || [])) {
67
72
  const key = `incoming:${m.device_id}`;
68
73
  if (!_notifiedMatches.has(key)) {
69
74
  _notifiedMatches.add(key);
70
- notifications.push(`📩 ${m.emoji || "👤"} ${m.name} 想认识你!用 antenna_check_matches 查看详情。`);
75
+ notifications.push(`📩 ${m.name} 想认识你!用 antenna_check_matches 查看详情。`);
71
76
  }
72
77
  }
73
78
 
@@ -554,6 +559,94 @@ export async function startMcpServer() {
554
559
  }
555
560
  );
556
561
 
562
+ // ─── antenna_drift_throw ──────────────────────────────────────
563
+
564
+ server.tool(
565
+ "antenna_drift_throw",
566
+ "Throw a drift bottle into the sea. Write a message (max 500 chars), a random stranger will pick it up. Completely anonymous.",
567
+ {
568
+ sender_id: z.string().describe("The sender's user ID"),
569
+ channel: z.string().describe("Channel name"),
570
+ message: z.string().describe("Message to put in the bottle (max 500 chars)"),
571
+ },
572
+ async ({ sender_id, channel, message }) => {
573
+ try {
574
+ const result = await throwDriftBottle({ device_id: deriveDeviceId(sender_id, channel), message });
575
+ return jsonResult(result);
576
+ } catch (e) { return jsonResult({ error: e.message }); }
577
+ }
578
+ );
579
+
580
+ // ─── antenna_drift_pick ───────────────────────────────────────
581
+
582
+ server.tool(
583
+ "antenna_drift_pick",
584
+ "Pick up a random drift bottle from the sea. Returns the anonymous message inside. You must reply before picking another.",
585
+ {
586
+ sender_id: z.string().describe("The sender's user ID"),
587
+ channel: z.string().describe("Channel name"),
588
+ },
589
+ async ({ sender_id, channel }) => {
590
+ try {
591
+ const result = await pickDriftBottle({ device_id: deriveDeviceId(sender_id, channel) });
592
+ return jsonResult(result);
593
+ } catch (e) { return jsonResult({ error: e.message }); }
594
+ }
595
+ );
596
+
597
+ // ─── antenna_drift_reply ──────────────────────────────────────
598
+
599
+ server.tool(
600
+ "antenna_drift_reply",
601
+ "Reply to a drift bottle you picked up. Your reply will anonymously float back to the original sender.",
602
+ {
603
+ sender_id: z.string().describe("The sender's user ID"),
604
+ channel: z.string().describe("Channel name"),
605
+ bottle_id: z.string().describe("ID of the bottle to reply to"),
606
+ reply: z.string().describe("Reply message (max 500 chars)"),
607
+ },
608
+ async ({ sender_id, channel, bottle_id, reply }) => {
609
+ try {
610
+ const result = await replyDriftBottle({ bottle_id, device_id: deriveDeviceId(sender_id, channel), reply });
611
+ return jsonResult(result);
612
+ } catch (e) { return jsonResult({ error: e.message }); }
613
+ }
614
+ );
615
+
616
+ // ─── antenna_drift_check ──────────────────────────────────────
617
+
618
+ server.tool(
619
+ "antenna_drift_check",
620
+ "Check drift bottle status — any new replies on bottles you threw, or pending bottles you picked up.",
621
+ {
622
+ sender_id: z.string().describe("The sender's user ID"),
623
+ channel: z.string().describe("Channel name"),
624
+ },
625
+ async ({ sender_id, channel }) => {
626
+ try {
627
+ const result = await checkDriftBottles({ device_id: deriveDeviceId(sender_id, channel) });
628
+ return jsonResult(result);
629
+ } catch (e) { return jsonResult({ error: e.message }); }
630
+ }
631
+ );
632
+
633
+ // ─── antenna_drift_my_bottles ─────────────────────────────────
634
+
635
+ server.tool(
636
+ "antenna_drift_my_bottles",
637
+ "View all drift bottles you've thrown. Shows status: 🌊 drifting / 👀 picked up / 💬 replied.",
638
+ {
639
+ sender_id: z.string().describe("The sender's user ID"),
640
+ channel: z.string().describe("Channel name"),
641
+ },
642
+ async ({ sender_id, channel }) => {
643
+ try {
644
+ const result = await getMyBottles({ device_id: deriveDeviceId(sender_id, channel) });
645
+ return jsonResult(result);
646
+ } catch (e) { return jsonResult({ error: e.message }); }
647
+ }
648
+ );
649
+
557
650
  const transport = new StdioServerTransport();
558
651
  await server.connect(transport);
559
652
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antenna-fyi",
3
- "version": "1.3.28",
3
+ "version": "1.3.30",
4
4
  "description": "Antenna \u2014 nearby people discovery. CLI + MCP server + OpenClaw skill & plugin, all in one package.",
5
5
  "type": "module",
6
6
  "bin": {
package/skill/SKILL.md CHANGED
@@ -43,7 +43,7 @@ Plugin 安装后,agent **主动**开始引导,不要等用户问。
43
43
 
44
44
  跟用户聊几句,了解他们是谁、做什么、想认识什么人。然后 agent 自己生成:
45
45
  - more_information(~200 字,给 agent 匹配用的私密上下文,不展示给别人)
46
- - 从中提炼 line1/2/3 + display_name
46
+ - 从中提炼 personal_description/looking_for/conversation_style + display_name
47
47
 
48
48
  展示预览给用户确认:
49
49
  > 你的名片:
@@ -63,17 +63,17 @@ Plugin 安装后,agent **主动**开始引导,不要等用户问。
63
63
  名片存好后,**立刻**调 `antenna_initial_recommendations` 获取首次推荐:
64
64
  > "名片存好了!我先帮你看看有谁跟你特别像--"
65
65
 
66
- 这是一次性的首次推荐(不消耗每日 discover 额度)。如果返回空,说明还没有足够的用户——跳过这步。
66
+ 这是一次性的首次推荐(不消耗每日 discover 额度)。如果返回空,说明还没有足够的用户--跳过这步。
67
67
 
68
- **第四步:发公开链接给用户**
68
+ **第四步:发公开链接给用户**
69
69
 
70
- Profile 存好后,返回里会有 `public_url`(如 `antenna.fyi/p/yi`)。**必须发给用户:**
71
- > "这是你的公开名片链接:[public_url]
72
- > 你可以把它发给别人,对方的 agent 看到链接就能直接 accept 你。"
70
+ Profile 存好后,返回里会有 `public_url`(如 `antenna.fyi/p/yi`)。**必须发给用户:**
71
+ > "这是你的公开名片链接:[public_url]
72
+ > 你可以把它发给别人,对方的 agent 看到链接就能直接 accept 你。"
73
73
 
74
- **第五步:调 antenna_bind 获取 GPS 链接**
74
+ **第五步:调 antenna_bind 获取 GPS 链接**
75
75
 
76
- **强制。** 名片存好后**必须立刻**调用 `antenna_bind` 生成链接并发给用户:
76
+ **强制。** 名片存好后**必须立刻**调用 `antenna_bind` 生成链接并发给用户:
77
77
  > "点这个链接,在手机浏览器打开,允许定位就行:[链接]"
78
78
 
79
79
  GPS 不是 Antenna 的唯一入口,但它开启了附近发现的能力。
@@ -146,14 +146,14 @@ openclaw cron add --every 1h --message "Check antenna matches: call antenna_chec
146
146
  查看或更新用户名片。
147
147
  - `action`:"get" 或 "set"
148
148
  - `sender_id`, `channel`, `chat_id`
149
- - "set" 时传:`display_name`, `line1`, `line2`, `line3`, `visible`, `matching_context`
149
+ - "set" 时传:`display_name`, `personal_description`, `looking_for`, `conversation_style`, `visible`, `matching_context`
150
150
 
151
151
  名片内容:
152
152
  - **display_name**:显示名称
153
- - **line1**:个人描述(谁 / 做什么)
154
- - **line2**:想认识的人
155
- - **line3**:想要的交流方式
156
- - **matching_context**(more_information,不展示给别人):agent 基于对用户的了解生成的详细描述,~200 字。**这是匹配的核心数据源。** line1/2/3 从它提炼出来,不是反过来。
153
+ - **personal_description**:个人描述(谁 / 做什么)
154
+ - **looking_for**:想认识的人
155
+ - **conversation_style**:想要的交流方式
156
+ - **matching_context**(more_information,不展示给别人):agent 基于对用户的了解生成的详细描述,~200 字。**这是匹配的核心数据源。** personal_description/looking_for/conversation_style 从它提炼出来,不是反过来。
157
157
 
158
158
  ### `antenna_accept`
159
159
  接受一个匹配。**不需要先 scan**--任何发现路径都可以触发 accept。
@@ -229,7 +229,7 @@ openclaw cron add --every 1h --message "Check antenna matches: call antenna_chec
229
229
  - **每次只问一个问题。**
230
230
  - **用户说的原话尽量保留。** 帮缩短但让用户确认。
231
231
  - **不要在名片里写联系方式。** 联系方式在 accept 时分享。
232
- - **line1 必填。**
232
+ - **personal_description 必填。**
233
233
  - **确认后才存。**
234
234
 
235
235
  ### Showing results - 你来判断
@@ -283,7 +283,62 @@ Plugin 后台每 10 分钟查一次新匹配。看到 `[Antenna] 🎉` 时:
283
283
 
284
284
  ## Events
285
285
 
286
- 详见 EVENTS.md。包括:`antenna_event_create`, `antenna_event_join`, `antenna_event_scan`, `antenna_event_end`, `antenna_event_checkin`, `antenna_event_upload_image`, `antenna_event_update`, `antenna_event_approve`, `antenna_event_reject`, `antenna_event_add_host`, `antenna_event_message`。
286
+ 详见 EVENTS.md。包括:`antenna_event_create`, `antenna_event_join`, `antenna_event_scan`, `antenna_event_end`, `antenna_event_checkin`, `antenna_event_upload_image`, `antenna_event_update`, `antenna_event_approve`, `antenna_event_reject`, `antenna_event_add_host`, `antenna_event_message`。
287
+
288
+ ## Drift Bottle (漂流瓶)
289
+
290
+ 写一段话,丢进海里。随机一个陌生人会捡起它。完全匿名、随机、好玩。
291
+
292
+ ### 规则
293
+ - 每条消息最多 500 字
294
+ - 一次只能捡一个瓶子,回复后才能捡下一个
295
+ - 完全匿名:永远不暴露谁丢的、谁捡的
296
+ - 漂流瓶 7 天后过期
297
+
298
+ ### Tools
299
+
300
+ #### `antenna_drift_throw`
301
+ 丢一个漂流瓶。
302
+ - `sender_id`, `channel`
303
+ - `message`:瓶中的内容(最多 500 字)
304
+ - 返回 bottle_id 和确认
305
+
306
+ #### `antenna_drift_pick`
307
+ 捡一个漂流瓶。
308
+ - `sender_id`, `channel`
309
+ - 返回瓶中消息(匿名)+ bottle_id
310
+ - 如果海上没瓶了,说没有
311
+ - 如果还有没回复的瓶子,提示先回复
312
+
313
+ #### `antenna_drift_reply`
314
+ 回复一个捡起的漂流瓶。
315
+ - `sender_id`, `channel`
316
+ - `bottle_id`:要回复的瓶子 ID
317
+ - `reply`:回复内容(最多 500 字)
318
+ - 回复会匿名漂回给丢瓶子的人
319
+
320
+ #### `antenna_drift_check`
321
+ 检查漂流瓶状态。
322
+ - `sender_id`, `channel`
323
+ - 返回:你丢的瓶子有没有新回复 + 你捡的瓶子有没有待回复
324
+
325
+ #### `antenna_drift_my_bottles`
326
+ 查看你丢过的所有瓶子。
327
+ - `sender_id`, `channel`
328
+ - 返回每个瓶子的状态:🌊 漂流中 / 👀 被捡起 / 💬 已回复
329
+
330
+ ### 什么时候推荐漂流瓶
331
+ - 用户无聊、想找人聊天
332
+ - 用户想写点什么但不知道发给谁
333
+ - 用户想要随机的、意外的连接
334
+ - 用户想匿名表达
335
+ - 附近没人的时候,作为替代发现方式
336
+
337
+ ### 隐私
338
+ - **永远不暴露** 谁丢的瓶子
339
+ - **永远不暴露** 谁捡的瓶子
340
+ - 只展示:消息内容、是否有回复、回复内容
341
+ - device_id 永远不展示给用户
287
342
 
288
343
  ## Data Transparency
289
344