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 +3 -0
- package/lib/cli.js +81 -21
- package/lib/core.js +39 -8
- package/lib/mcp.js +95 -2
- package/package.json +1 -1
- package/skill/SKILL.md +70 -15
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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! ${
|
|
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(`📩 ${
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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:
|
|
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.
|
|
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.
|
|
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
package/skill/SKILL.md
CHANGED
|
@@ -43,7 +43,7 @@ Plugin 安装后,agent **主动**开始引导,不要等用户问。
|
|
|
43
43
|
|
|
44
44
|
跟用户聊几句,了解他们是谁、做什么、想认识什么人。然后 agent 自己生成:
|
|
45
45
|
- more_information(~200 字,给 agent 匹配用的私密上下文,不展示给别人)
|
|
46
|
-
- 从中提炼
|
|
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
|
-
|
|
66
|
+
这是一次性的首次推荐(不消耗每日 discover 额度)。如果返回空,说明还没有足够的用户--跳过这步。
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
**第四步:发公开链接给用户**
|
|
69
69
|
|
|
70
|
-
Profile
|
|
71
|
-
> "
|
|
72
|
-
>
|
|
70
|
+
Profile 存好后,返回里会有 `public_url`(如 `antenna.fyi/p/yi`)。**必须发给用户:**
|
|
71
|
+
> "这是你的公开名片链接:[public_url]
|
|
72
|
+
> 你可以把它发给别人,对方的 agent 看到链接就能直接 accept 你。"
|
|
73
73
|
|
|
74
|
-
|
|
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`, `
|
|
149
|
+
- "set" 时传:`display_name`, `personal_description`, `looking_for`, `conversation_style`, `visible`, `matching_context`
|
|
150
150
|
|
|
151
151
|
名片内容:
|
|
152
152
|
- **display_name**:显示名称
|
|
153
|
-
- **
|
|
154
|
-
- **
|
|
155
|
-
- **
|
|
156
|
-
- **matching_context**(more_information,不展示给别人):agent 基于对用户的了解生成的详细描述,~200 字。**这是匹配的核心数据源。**
|
|
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
|
-
- **
|
|
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
|
|
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
|
|