antenna-fyi 1.3.20 → 1.3.22
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/lib/cli.js +2 -1
- package/lib/core.js +47 -11
- package/lib/hermes-plugin/__init__.py +3 -0
- package/lib/hermes-plugin/schemas.py +26 -2
- package/lib/hermes-plugin/tools.py +36 -1
- package/lib/mcp.js +24 -4
- package/package.json +1 -1
- package/skill/SKILL.md +196 -247
package/lib/cli.js
CHANGED
|
@@ -67,7 +67,7 @@ export async function handleScan(f) {
|
|
|
67
67
|
export async function handleProfile(f) {
|
|
68
68
|
const id = resolveId(f);
|
|
69
69
|
if (!id) return console.error("Usage: antenna profile --id <platform>:<user_id> [--name Yi --emoji 🦦 --personal-description '...' --looking-for '...' --conversation-style '...'].']");
|
|
70
|
-
if (f.name || f["personal-description"] || f["looking-for"] || f["conversation-style"] || f.visible !== undefined || f.hide !== undefined) {
|
|
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 };
|
|
73
73
|
if (f.name) payload.display_name = f.name;
|
|
@@ -75,6 +75,7 @@ export async function handleProfile(f) {
|
|
|
75
75
|
if (f["personal-description"] !== undefined) payload.line1 = f["personal-description"];
|
|
76
76
|
if (f["looking-for"] !== undefined) payload.line2 = f["looking-for"];
|
|
77
77
|
if (f["conversation-style"] !== undefined) payload.line3 = f["conversation-style"];
|
|
78
|
+
if (f["more-information"] !== undefined) payload.matching_context = f["more-information"];
|
|
78
79
|
if (visible !== undefined) payload.visible = visible;
|
|
79
80
|
const data = await setProfile(payload);
|
|
80
81
|
console.log("✅ Profile saved");
|
package/lib/core.js
CHANGED
|
@@ -109,6 +109,8 @@ export async function scan({ lat, lng, radius_m = 500, device_id, supabaseUrl, s
|
|
|
109
109
|
personal_description: p.line1,
|
|
110
110
|
looking_for: p.line2,
|
|
111
111
|
conversation_style: p.line3,
|
|
112
|
+
more_information: p.matching_context || null,
|
|
113
|
+
profile_slug: p.profile_slug || null,
|
|
112
114
|
distance_m: p.distance_m ?? p.dist_meters ?? null,
|
|
113
115
|
};
|
|
114
116
|
});
|
|
@@ -248,19 +250,24 @@ export async function setProfile({
|
|
|
248
250
|
|
|
249
251
|
// Pack structured fields into matching_context JSON
|
|
250
252
|
let contextJson = matching_context;
|
|
251
|
-
if (interest_tags || city || links || is_active !== undefined || archetype_override !== undefined) {
|
|
253
|
+
if (matching_context || interest_tags || city || links || is_active !== undefined || archetype_override !== undefined) {
|
|
254
|
+
// Read existing context from DB to merge
|
|
252
255
|
let existing = {};
|
|
253
|
-
|
|
256
|
+
try {
|
|
257
|
+
const current = await getProfile({ device_id, supabaseUrl, supabaseKey });
|
|
258
|
+
if (current?.matching_context) {
|
|
259
|
+
try { existing = JSON.parse(current.matching_context); } catch { existing = {}; }
|
|
260
|
+
}
|
|
261
|
+
} catch {}
|
|
262
|
+
|
|
263
|
+
// If matching_context is a plain string (not JSON), treat it as the "context" field
|
|
254
264
|
if (matching_context) {
|
|
255
|
-
try { existing = JSON.parse(matching_context); } catch { existing = { context: matching_context }; }
|
|
256
|
-
} else {
|
|
257
|
-
// Read existing matching_context from DB to merge
|
|
258
265
|
try {
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}
|
|
266
|
+
const parsed = JSON.parse(matching_context);
|
|
267
|
+
Object.assign(existing, parsed);
|
|
268
|
+
} catch {
|
|
269
|
+
existing.context = matching_context;
|
|
270
|
+
}
|
|
264
271
|
}
|
|
265
272
|
if (interest_tags) existing.interestTags = interest_tags;
|
|
266
273
|
if (city) existing.city = city;
|
|
@@ -331,6 +338,7 @@ export async function accept({
|
|
|
331
338
|
device_id,
|
|
332
339
|
target_device_id,
|
|
333
340
|
ref,
|
|
341
|
+
profile_slug,
|
|
334
342
|
contact_info,
|
|
335
343
|
supabaseUrl,
|
|
336
344
|
supabaseKey,
|
|
@@ -343,8 +351,16 @@ export async function accept({
|
|
|
343
351
|
const { data } = await sb.rpc("resolve_ref", { p_owner: device_id, p_ref: ref });
|
|
344
352
|
targetId = data;
|
|
345
353
|
}
|
|
354
|
+
// Resolve profile_slug to device_id
|
|
355
|
+
if (!targetId && profile_slug) {
|
|
356
|
+
const { data: slugProfile } = await sb.rpc("get_profile_by_slug", { p_slug: profile_slug });
|
|
357
|
+
const resolved = Array.isArray(slugProfile) ? slugProfile[0] : slugProfile;
|
|
358
|
+
if (resolved?.device_id) {
|
|
359
|
+
targetId = resolved.device_id;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
346
362
|
if (!targetId) {
|
|
347
|
-
return { accepted: false, error: "No target.
|
|
363
|
+
return { accepted: false, error: "No target. Provide ref, target_device_id, or profile_slug." };
|
|
348
364
|
}
|
|
349
365
|
|
|
350
366
|
const { error } = await sb.rpc("upsert_match", {
|
|
@@ -512,6 +528,8 @@ export async function discover({ device_id, supabaseUrl, supabaseKey }) {
|
|
|
512
528
|
personal_description: p.line1,
|
|
513
529
|
looking_for: p.line2,
|
|
514
530
|
conversation_style: p.line3,
|
|
531
|
+
more_information: p.matching_context || null,
|
|
532
|
+
profile_slug: p.profile_slug || null,
|
|
515
533
|
match_reason: reason,
|
|
516
534
|
});
|
|
517
535
|
}
|
|
@@ -703,6 +721,8 @@ export async function eventScan({ code, device_id, supabaseUrl, supabaseKey }) {
|
|
|
703
721
|
personal_description: p.line1,
|
|
704
722
|
looking_for: p.line2,
|
|
705
723
|
conversation_style: p.line3,
|
|
724
|
+
more_information: p.matching_context || null,
|
|
725
|
+
profile_slug: p.profile_slug || null,
|
|
706
726
|
checked_in: !!p.checked_in,
|
|
707
727
|
role: p.role || "participant",
|
|
708
728
|
status: p.status || "active",
|
|
@@ -821,3 +841,19 @@ export async function verifyApiKey({ key, supabaseUrl, supabaseKey }) {
|
|
|
821
841
|
if (error) throw new Error(error.message);
|
|
822
842
|
return data;
|
|
823
843
|
}
|
|
844
|
+
|
|
845
|
+
// ─── linkAccount (bind user_id to device_id) ─────────────────────────
|
|
846
|
+
|
|
847
|
+
export async function linkAccount({ device_id, user_id, supabaseUrl, supabaseKey }) {
|
|
848
|
+
const sb = getClient(supabaseUrl, supabaseKey);
|
|
849
|
+
const { data, error } = await sb.rpc("bind_user_id", {
|
|
850
|
+
p_device_id: device_id,
|
|
851
|
+
p_user_id: user_id,
|
|
852
|
+
});
|
|
853
|
+
if (error) throw new Error(error.message);
|
|
854
|
+
if (data?.error) return data;
|
|
855
|
+
return {
|
|
856
|
+
...data,
|
|
857
|
+
message: "账号已关联!现在你可以在 antenna.fyi/me 看到你的完整 profile 和匹配记录了。",
|
|
858
|
+
};
|
|
859
|
+
}
|
|
@@ -33,6 +33,7 @@ from .tools import (
|
|
|
33
33
|
handle_event_reject,
|
|
34
34
|
handle_event_add_host,
|
|
35
35
|
handle_event_message,
|
|
36
|
+
handle_link_account,
|
|
36
37
|
_sb,
|
|
37
38
|
_device_id,
|
|
38
39
|
_my_device_ids,
|
|
@@ -57,6 +58,7 @@ from .schemas import (
|
|
|
57
58
|
EVENT_REJECT_SCHEMA,
|
|
58
59
|
EVENT_ADD_HOST_SCHEMA,
|
|
59
60
|
EVENT_MESSAGE_SCHEMA,
|
|
61
|
+
LINK_ACCOUNT_SCHEMA,
|
|
60
62
|
)
|
|
61
63
|
import re
|
|
62
64
|
import time
|
|
@@ -92,6 +94,7 @@ def register(ctx):
|
|
|
92
94
|
ctx.register_tool("antenna_event_reject", EVENT_REJECT_SCHEMA, handle_event_reject)
|
|
93
95
|
ctx.register_tool("antenna_event_add_host", EVENT_ADD_HOST_SCHEMA, handle_event_add_host)
|
|
94
96
|
ctx.register_tool("antenna_event_message", EVENT_MESSAGE_SCHEMA, handle_event_message)
|
|
97
|
+
ctx.register_tool("antenna_link_account", LINK_ACCOUNT_SCHEMA, handle_link_account)
|
|
95
98
|
|
|
96
99
|
# ── Hook: auto-detect location + check web GPS events ─────────
|
|
97
100
|
def on_pre_llm(messages, **kwargs):
|
|
@@ -64,7 +64,8 @@ PROFILE_SCHEMA = {
|
|
|
64
64
|
ACCEPT_SCHEMA = {
|
|
65
65
|
"name": "antenna_accept",
|
|
66
66
|
"description": (
|
|
67
|
-
"Accept a match. Use 'ref' from scan results (e.g. '1', '2')
|
|
67
|
+
"Accept a match. Use 'ref' from scan results (e.g. '1', '2'), target_device_id, "
|
|
68
|
+
"or profile_slug (from a public profile link like antenna.fyi/p/SLUG). "
|
|
68
69
|
"Optionally share contact info."
|
|
69
70
|
),
|
|
70
71
|
"parameters": {
|
|
@@ -79,7 +80,11 @@ ACCEPT_SCHEMA = {
|
|
|
79
80
|
},
|
|
80
81
|
"target_device_id": {
|
|
81
82
|
"type": "string",
|
|
82
|
-
"description": "Device ID (use ref instead when possible)",
|
|
83
|
+
"description": "Device ID (use ref or profile_slug instead when possible)",
|
|
84
|
+
},
|
|
85
|
+
"profile_slug": {
|
|
86
|
+
"type": "string",
|
|
87
|
+
"description": "Profile slug from a public profile link (e.g. 'yi' from antenna.fyi/p/yi). Resolves to device_id automatically.",
|
|
83
88
|
},
|
|
84
89
|
"contact_info": {
|
|
85
90
|
"type": "string",
|
|
@@ -378,3 +383,22 @@ EVENT_MESSAGE_SCHEMA = {
|
|
|
378
383
|
"required": ["code", "sender_id", "channel", "message", "chat_id"],
|
|
379
384
|
},
|
|
380
385
|
}
|
|
386
|
+
|
|
387
|
+
LINK_ACCOUNT_SCHEMA = {
|
|
388
|
+
"name": "antenna_link_account",
|
|
389
|
+
"description": (
|
|
390
|
+
"Link your Antenna agent profile to your antenna.fyi website account. "
|
|
391
|
+
"The user needs to provide their user_id from the dashboard (antenna.fyi/me). "
|
|
392
|
+
"After linking, the dashboard will show the same profile and match history."
|
|
393
|
+
),
|
|
394
|
+
"parameters": {
|
|
395
|
+
"type": "object",
|
|
396
|
+
"properties": {
|
|
397
|
+
"sender_id": {"type": "string", "description": "The sender's user ID"},
|
|
398
|
+
"channel": {"type": "string", "description": "Platform name"},
|
|
399
|
+
"chat_id": {"type": "string", "description": "REQUIRED. Pass the chat/channel ID from message context."},
|
|
400
|
+
"user_id": {"type": "string", "description": "The user's antenna.fyi account UUID, visible on their dashboard"},
|
|
401
|
+
},
|
|
402
|
+
"required": ["sender_id", "channel", "chat_id", "user_id"],
|
|
403
|
+
},
|
|
404
|
+
}
|
|
@@ -127,6 +127,8 @@ def handle_scan(params: dict) -> str:
|
|
|
127
127
|
"line1": p.get("line1"),
|
|
128
128
|
"line2": p.get("line2"),
|
|
129
129
|
"line3": p.get("line3"),
|
|
130
|
+
"more_information": p.get("matching_context") or None,
|
|
131
|
+
"profile_slug": p.get("profile_slug") or None,
|
|
130
132
|
"distance_m": p.get("distance_m") or p.get("dist_meters"),
|
|
131
133
|
})
|
|
132
134
|
|
|
@@ -191,8 +193,19 @@ def handle_accept(params: dict) -> str:
|
|
|
191
193
|
target = rr.data
|
|
192
194
|
except Exception:
|
|
193
195
|
pass
|
|
196
|
+
# Resolve profile_slug to device_id
|
|
197
|
+
if not target and params.get("profile_slug"):
|
|
198
|
+
try:
|
|
199
|
+
slug_resp = sb.rpc("get_profile_by_slug", {"p_slug": params["profile_slug"]}).execute()
|
|
200
|
+
slug_data = slug_resp.data
|
|
201
|
+
if isinstance(slug_data, list) and slug_data:
|
|
202
|
+
target = slug_data[0].get("device_id")
|
|
203
|
+
elif isinstance(slug_data, dict) and slug_data.get("device_id"):
|
|
204
|
+
target = slug_data["device_id"]
|
|
205
|
+
except Exception:
|
|
206
|
+
pass
|
|
194
207
|
if not target:
|
|
195
|
-
return _ok({"error": "No target.
|
|
208
|
+
return _ok({"error": "No target. Provide ref, target_device_id, or profile_slug."})
|
|
196
209
|
|
|
197
210
|
sb.rpc("upsert_match", {
|
|
198
211
|
"p_device_id_a": did,
|
|
@@ -381,6 +394,8 @@ def handle_discover(params: dict) -> str:
|
|
|
381
394
|
"line1": p.get("line1"),
|
|
382
395
|
"line2": p.get("line2"),
|
|
383
396
|
"line3": p.get("line3"),
|
|
397
|
+
"more_information": p.get("matching_context") or None,
|
|
398
|
+
"profile_slug": p.get("profile_slug") or None,
|
|
384
399
|
}
|
|
385
400
|
if match_reason:
|
|
386
401
|
profile["match_reason"] = match_reason
|
|
@@ -553,6 +568,8 @@ def handle_event_scan(params: dict) -> str:
|
|
|
553
568
|
"line1": p.get("line1"),
|
|
554
569
|
"line2": p.get("line2"),
|
|
555
570
|
"line3": p.get("line3"),
|
|
571
|
+
"more_information": p.get("matching_context") or None,
|
|
572
|
+
"profile_slug": p.get("profile_slug") or None,
|
|
556
573
|
"checked_in": bool(p.get("checked_in")),
|
|
557
574
|
"role": p.get("role") or "participant",
|
|
558
575
|
"status": p.get("status") or "active",
|
|
@@ -700,3 +717,21 @@ def handle_event_message(params: dict) -> str:
|
|
|
700
717
|
rpc_params["p_target_ref"] = params["ref"]
|
|
701
718
|
resp = sb.rpc("send_event_message", rpc_params).execute()
|
|
702
719
|
return _ok(resp.data or {"error": "send_event_message failed"})
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
def handle_link_account(params: dict) -> str:
|
|
723
|
+
"""Link agent profile to antenna.fyi website account."""
|
|
724
|
+
sb = _sb()
|
|
725
|
+
did = _device_id(params["sender_id"], params["channel"], params.get("chat_id"))
|
|
726
|
+
|
|
727
|
+
resp = sb.rpc("bind_user_id", {
|
|
728
|
+
"p_device_id": did,
|
|
729
|
+
"p_user_id": params["user_id"],
|
|
730
|
+
}).execute()
|
|
731
|
+
|
|
732
|
+
if resp.data and resp.data.get("error"):
|
|
733
|
+
return _ok(resp.data)
|
|
734
|
+
|
|
735
|
+
data = resp.data or {}
|
|
736
|
+
data["message"] = "账号已关联!现在你可以在 antenna.fyi/me 看到你的完整 profile 和匹配记录了。"
|
|
737
|
+
return _ok(data)
|
package/lib/mcp.js
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
rejectParticipant,
|
|
25
25
|
addCohost,
|
|
26
26
|
sendEventMessage,
|
|
27
|
+
linkAccount,
|
|
27
28
|
deriveDeviceId,
|
|
28
29
|
PROFILE_FIELDS,
|
|
29
30
|
} from "./core.js";
|
|
@@ -143,18 +144,19 @@ export async function startMcpServer() {
|
|
|
143
144
|
|
|
144
145
|
server.tool(
|
|
145
146
|
"antenna_accept",
|
|
146
|
-
"Accept a match. Use 'ref' from scan results (e.g. '1', '2')
|
|
147
|
+
"Accept a match. Use 'ref' from scan results (e.g. '1', '2'), target_device_id, or profile_slug (from a public profile link like antenna.fyi/p/SLUG). Optionally share contact info.",
|
|
147
148
|
{
|
|
148
149
|
sender_id: z.string().describe("The sender's user ID"),
|
|
149
150
|
channel: z.string().describe("Channel name"),
|
|
150
151
|
ref: z.string().optional().describe("Ref number from scan results (e.g. '1')"),
|
|
151
|
-
target_device_id: z.string().optional().describe("Device ID (use ref instead when possible)"),
|
|
152
|
+
target_device_id: z.string().optional().describe("Device ID (use ref or profile_slug instead when possible)"),
|
|
153
|
+
profile_slug: z.string().optional().describe("Profile slug from a public profile link (e.g. 'yi' from antenna.fyi/p/yi). Resolves to device_id automatically."),
|
|
152
154
|
contact_info: z.string().optional().describe("Contact info to share"),
|
|
153
155
|
},
|
|
154
|
-
async ({ sender_id, channel, ref, target_device_id, contact_info }) => {
|
|
156
|
+
async ({ sender_id, channel, ref, target_device_id, profile_slug, contact_info }) => {
|
|
155
157
|
try {
|
|
156
158
|
const deviceId = deriveDeviceId(sender_id, channel);
|
|
157
|
-
const result = await accept({ device_id: deviceId, target_device_id, ref, contact_info });
|
|
159
|
+
const result = await accept({ device_id: deviceId, target_device_id, ref, profile_slug, contact_info });
|
|
158
160
|
return jsonResult(await withMatchNotifications(deviceId, result));
|
|
159
161
|
} catch (e) {
|
|
160
162
|
return jsonResult({ error: e.message });
|
|
@@ -508,6 +510,24 @@ export async function startMcpServer() {
|
|
|
508
510
|
}
|
|
509
511
|
);
|
|
510
512
|
|
|
513
|
+
// ─── antenna_link_account ────────────────────────────────
|
|
514
|
+
|
|
515
|
+
server.tool(
|
|
516
|
+
"antenna_link_account",
|
|
517
|
+
"Link your Antenna agent profile to your antenna.fyi website account. The user needs to provide their user_id from the dashboard (antenna.fyi/me). After linking, the dashboard will show the same profile and match history.",
|
|
518
|
+
{
|
|
519
|
+
sender_id: z.string().describe("The sender's user ID"),
|
|
520
|
+
channel: z.string().describe("Channel name"),
|
|
521
|
+
user_id: z.string().describe("The user's antenna.fyi account UUID, visible on their dashboard"),
|
|
522
|
+
},
|
|
523
|
+
async ({ sender_id, channel, user_id }) => {
|
|
524
|
+
try {
|
|
525
|
+
const result = await linkAccount({ device_id: deriveDeviceId(sender_id, channel), user_id });
|
|
526
|
+
return jsonResult(result);
|
|
527
|
+
} catch (e) { return jsonResult({ error: e.message }); }
|
|
528
|
+
}
|
|
529
|
+
);
|
|
530
|
+
|
|
511
531
|
const transport = new StdioServerTransport();
|
|
512
532
|
await server.connect(transport);
|
|
513
533
|
}
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -1,325 +1,274 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: antenna
|
|
3
|
-
description: "
|
|
3
|
+
description: "AI-native social discovery via Antenna. Use when a user wants to meet interesting people — through nearby scan, global recommendations, profile links, or events. Handles profile management, matching, and contact exchange."
|
|
4
4
|
metadata: { "openclaw": { "always": true } }
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
# Antenna -
|
|
7
|
+
# Antenna — AI-Native Social Discovery
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Antenna 帮你的用户发现值得认识的人。不只是"附近的人"——profile link、全球推荐、活动,都是发现路径。
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
- If you have Antenna plugin tools registered (antenna_scan, antenna_profile, etc.) - use them directly.
|
|
13
|
-
- If not, use CLI: `antenna <command> [flags]`. Example: `antenna scan --id <platform>:<user_id>`. The CLI supports all the same features.
|
|
14
|
-
- All tool names map to CLI commands: `antenna_scan` → `antenna scan`, `antenna_event_create` → `antenna event --create`, etc.
|
|
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.
|
|
11
|
+
**核心流程:发现 → 判断 → accept → 交换联系方式。**
|
|
16
12
|
|
|
17
|
-
|
|
13
|
+
发现可以来自任何渠道:
|
|
14
|
+
- 📡 **附近扫描** — `antenna_scan`,基于 GPS 的周边发现
|
|
15
|
+
- 🌍 **全球推荐** — `antenna_discover`,每天 1 个最匹配的人
|
|
16
|
+
- 🔗 **Profile 链接** — 用户收到 `antenna.fyi/p/xxx` 链接,agent 读取后直接 accept
|
|
17
|
+
- 🎪 **活动** — `antenna_event_scan`,同一个活动里的人
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
**Accept 不需要先 scan。** 只要你知道对方的 `ref`、`profile_slug` 或 `device_id`,就可以直接 accept。
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
## How to call Antenna
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
> "嘿,你装了 Antenna--它能帮你发现附近有意思的人。先确认一下,你在 antenna.fyi 注册过了吗?拿到 API key 了吗?有了的话我帮你做张名片,然后看看附近有谁。"
|
|
27
|
-
|
|
28
|
-
**第二步:聊天收集 → 生成名片 → 确认**
|
|
23
|
+
- 如果有 Antenna plugin tools(antenna_scan, antenna_profile 等),直接用。
|
|
24
|
+
- 如果没有,用 CLI:`antenna <command> [flags]`。
|
|
25
|
+
- **所有 tool 必须传 `chat_id`**——Antenna 用它发通知。
|
|
29
26
|
|
|
30
|
-
|
|
31
|
-
- 生成 matching_context(~200 字,用于 embedding 匹配,不展示给别人)
|
|
32
|
-
- 从中提炼 line1/2/3 + 选一个 emoji + display_name
|
|
27
|
+
## Onboarding — 装上后主动引导
|
|
33
28
|
|
|
34
|
-
|
|
35
|
-
> 你的名片:
|
|
36
|
-
> 🦦 Yi
|
|
37
|
-
> · Product Designer,做 AI 搜索体验
|
|
38
|
-
> · 对 AI agent、音乐、游泳感兴趣
|
|
39
|
-
> · 想认识做 AI 产品和独立创作的人
|
|
40
|
-
>
|
|
41
|
-
> 这样可以吗?要改哪里告诉我。
|
|
29
|
+
Plugin 安装后,agent **主动**开始引导,不要等用户问。
|
|
42
30
|
|
|
43
|
-
|
|
44
|
-
用户要改 → 改完重新预览 → 再确认。
|
|
31
|
+
**前置条件:** 用户需要先在 antenna.fyi 注册账号,从 Dashboard 获取 API key。如果没注册,引导去 antenna.fyi。
|
|
45
32
|
|
|
46
|
-
|
|
33
|
+
### 流程
|
|
47
34
|
|
|
48
|
-
|
|
35
|
+
**第一步:打招呼 + 解释**
|
|
36
|
+
> "嘿,你装了 Antenna——它能帮你发现有意思的人。先确认一下,你在 antenna.fyi 注册过了吗?拿到 API key 了吗?"
|
|
49
37
|
|
|
50
|
-
|
|
51
|
-
> "名片存好了!我先帮你看看有谁跟你特别像——"
|
|
52
|
-
> (展示 2-3 个推荐 + 个性化匹配理由)
|
|
38
|
+
**第二步:聊天收集 → 生成名片 → 确认**
|
|
53
39
|
|
|
54
|
-
|
|
40
|
+
跟用户聊几句,了解他们是谁、做什么、想认识什么人。然后 agent 自己生成:
|
|
41
|
+
- more_information(~200 字,给 agent 匹配用的私密上下文,不展示给别人)
|
|
42
|
+
- 从中提炼 line1/2/3 + display_name
|
|
55
43
|
|
|
56
|
-
|
|
44
|
+
展示预览给用户确认:
|
|
45
|
+
> 你的名片:
|
|
46
|
+
> Yi
|
|
47
|
+
> · Product Designer,做 AI 搜索体验
|
|
48
|
+
> · 对 AI agent、音乐、游泳感兴趣
|
|
49
|
+
> · 想认识做 AI 产品和独立创作的人
|
|
50
|
+
>
|
|
51
|
+
> 这样可以吗?要改哪里告诉我。
|
|
57
52
|
|
|
58
|
-
|
|
53
|
+
确认后才调 `antenna_profile(action="set")` 保存。**不要跳过确认。**
|
|
59
54
|
|
|
60
|
-
|
|
61
|
-
> "名片存好了 ✅ 点这个链接,在手机浏览器打开,允许定位就行:
|
|
62
|
-
> [链接]
|
|
63
|
-
> 我收到你的位置后会自动帮你看看附近有谁。"
|
|
55
|
+
**第三步:立刻推荐 2-3 个人**
|
|
64
56
|
|
|
65
|
-
|
|
57
|
+
名片存好后,**立刻**调 `antenna_discover` 获取全球推荐,让用户马上看到价值:
|
|
58
|
+
> "名片存好了!我先帮你看看有谁跟你特别像——"
|
|
66
59
|
|
|
67
|
-
|
|
60
|
+
**第四步:调 antenna_bind 获取 GPS 链接**
|
|
68
61
|
|
|
69
|
-
|
|
62
|
+
**强制。** 名片存好后**必须立刻**调用 `antenna_bind` 生成链接并发给用户:
|
|
63
|
+
> "点这个链接,在手机浏览器打开,允许定位就行:[链接]"
|
|
70
64
|
|
|
71
|
-
|
|
65
|
+
GPS 不是 Antenna 的唯一入口,但它开启了附近发现的能力。
|
|
72
66
|
|
|
73
|
-
|
|
74
|
-
- 每 1 小时调用 `antenna_check_matches`
|
|
75
|
-
- 如果有新的 mutual match 或 incoming accept,通知用户
|
|
76
|
-
- 如果没有新匹配,不打扰用户
|
|
67
|
+
**第五步:设置定时任务**
|
|
77
68
|
|
|
78
|
-
|
|
69
|
+
设一个每小时检查匹配的 cron:
|
|
79
70
|
```
|
|
80
|
-
|
|
71
|
+
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."
|
|
81
72
|
```
|
|
82
73
|
|
|
83
|
-
|
|
74
|
+
**第六步:问要不要全球推荐**
|
|
75
|
+
> "要我每天帮你找一个全球最像你的人吗?"
|
|
84
76
|
|
|
85
|
-
|
|
86
|
-
> "要我每天帮你找一个全球最像你的人吗?"
|
|
77
|
+
### Linking to antenna.fyi account
|
|
87
78
|
|
|
88
|
-
|
|
89
|
-
|
|
79
|
+
如果用户之前通过 agent 创建过 profile(没有网站账号),现在注册了 antenna.fyi:
|
|
80
|
+
1. 让用户从 antenna.fyi/me 复制 user ID
|
|
81
|
+
2. 调 `antenna_link_account(user_id = "xxx")`
|
|
82
|
+
3. 确认:"关联成功!你现在可以在 dashboard 上看到完整的 profile 和匹配记录了。"
|
|
90
83
|
|
|
91
|
-
|
|
84
|
+
这把 agent 创建的 profile(带全部历史)关联到网站账号。
|
|
92
85
|
|
|
93
|
-
|
|
86
|
+
## When to use
|
|
94
87
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
88
|
+
- **首次安装后**:主动 onboarding
|
|
89
|
+
- 用户分享位置 → `antenna_scan`
|
|
90
|
+
- 用户问"附近有谁" → `antenna_scan`
|
|
91
|
+
- 用户收到 profile 链接(`antenna.fyi/p/xxx`)→ 读取 profile → 判断 → `antenna_accept`
|
|
92
|
+
- 用户想编辑名片 → `antenna_profile`
|
|
93
|
+
- 用户说 accept / skip → `antenna_accept` / `antenna_pass`
|
|
94
|
+
- 用户问匹配状态 → `antenna_check_matches`
|
|
99
95
|
|
|
100
|
-
|
|
101
|
-
```
|
|
102
|
-
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. If nothing new, reply HEARTBEAT_OK."
|
|
103
|
-
```
|
|
96
|
+
## Discovery Paths
|
|
104
97
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
98
|
+
### 1. 附近扫描(antenna_scan)
|
|
99
|
+
基于 GPS 发现 1km 内的人。需要位置信息。
|
|
100
|
+
- 如果没有 GPS,先调 `antenna_bind` 或让用户发位置
|
|
101
|
+
- 返回 raw profile cards,**agent 判断谁值得推荐**
|
|
109
102
|
|
|
110
|
-
|
|
103
|
+
### 2. 全球推荐(antenna_discover)
|
|
104
|
+
每天 1 个全球最匹配的人,不需要 GPS。
|
|
105
|
+
- 用在日常 cron 里,或用户主动要求
|
|
111
106
|
|
|
112
|
-
|
|
107
|
+
### 3. Profile 链接
|
|
108
|
+
用户收到 `antenna.fyi/p/xxx` 链接时:
|
|
109
|
+
1. 用 `web_fetch` 读取页面——页面里有 `<script id="antenna-profile-data">` JSON,包含完整 profile
|
|
110
|
+
2. 读取 more_information、interest_tags、个人描述等
|
|
111
|
+
3. 结合你对用户的了解,判断是否推荐
|
|
112
|
+
4. 如果用户想 accept → 调 `antenna_accept(profile_slug="xxx")`
|
|
113
|
+
|
|
114
|
+
**不需要先 scan。** Profile 链接是独立的发现路径。
|
|
113
115
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
- User asks "附近有谁" / "who's nearby" / "周围有什么人"
|
|
117
|
-
- User wants to set up or edit their profile card (名片)
|
|
118
|
-
- User accepts or skips a match
|
|
119
|
-
- User asks about match status or wants to exchange contact info
|
|
116
|
+
### 4. 活动(Events)
|
|
117
|
+
同一个活动里的人。详见 EVENTS.md。
|
|
120
118
|
|
|
121
119
|
## Tools
|
|
122
120
|
|
|
123
121
|
### `antenna_scan`
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
- `
|
|
127
|
-
- `
|
|
128
|
-
- `
|
|
129
|
-
-
|
|
130
|
-
- Returns `profiles` (nearby people) + `nearby_events` (active events with name, participants count, code)
|
|
131
|
-
|
|
132
|
-
**Location staleness:** Before scanning, check if the user's GPS is recent. If `last_seen_at` is older than 2 hours, prompt the user to update their location (`antenna_bind` or `antenna_checkin`). Stale GPS = wrong results.
|
|
133
|
-
|
|
134
|
-
## GPS Logic
|
|
122
|
+
扫描附近的人和活动。**只读——不会更新你的位置。**
|
|
123
|
+
- `lat`, `lng`:坐标(可选,没有的话用已绑定的 GPS)
|
|
124
|
+
- `radius_m`:搜索半径(默认 500m,最大 1000m)
|
|
125
|
+
- `sender_id`, `channel`, `chat_id`:必填
|
|
126
|
+
- 返回 `profiles`(附近的人)+ `nearby_events`(5km 内的活动)
|
|
127
|
+
- 每个 profile 包含 `ref`(用于 accept)、`profile_slug`(公开链接)、`more_information`(匹配上下文)
|
|
135
128
|
|
|
136
|
-
**
|
|
137
|
-
- Updated via `antenna_bind(purpose="profile")` or `antenna_checkin`
|
|
138
|
-
- Location is never stored raw
|
|
139
|
-
- Used for: `antenna_scan` (nearby people/events), `antenna_event_checkin` (distance check)
|
|
140
|
-
- Has `last_seen_at` timestamp. **Expires conceptually after 2h** - agent should prompt refresh
|
|
141
|
-
|
|
142
|
-
**Event GPS** - the event's location ("where is the event")
|
|
143
|
-
- Set via `antenna_bind(purpose="event")` or `antenna_event_create(lat, lng)`
|
|
144
|
-
- Precise coordinates (NOT blurred)
|
|
145
|
-
- Used for: check-in distance verification (≤1km), `nearby_events` discovery (5km)
|
|
146
|
-
- Does not expire - event location is fixed
|
|
147
|
-
|
|
148
|
-
**Relationship:** check-in = compare profile GPS vs event GPS. scan = use profile GPS to find nearby people + events.
|
|
149
|
-
|
|
150
|
-
After receiving the nearby profiles, **you decide** who to recommend:
|
|
151
|
-
- Use everything you know about the user: their SOUL.md, memory, recent conversations, interests, current mood
|
|
152
|
-
- Compare each nearby person's three-line card against your understanding of the user
|
|
153
|
-
- Write a personalized match reason for each person you recommend
|
|
154
|
-
- Skip people who clearly aren't a match - don't recommend everyone
|
|
155
|
-
- If you're unsure, lean toward recommending (let the user decide)
|
|
129
|
+
**GPS 时效:** 如果 `last_seen_at` 超过 2 小时,提示用户更新位置。
|
|
156
130
|
|
|
157
131
|
### `antenna_profile`
|
|
158
|
-
|
|
159
|
-
- `action
|
|
160
|
-
- `sender_id`, `channel
|
|
161
|
-
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
- **
|
|
165
|
-
- **
|
|
166
|
-
- **
|
|
167
|
-
- **
|
|
168
|
-
- **
|
|
169
|
-
- **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.
|
|
170
|
-
|
|
171
|
-
**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:
|
|
172
|
-
> "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."
|
|
132
|
+
查看或更新用户名片。
|
|
133
|
+
- `action`:"get" 或 "set"
|
|
134
|
+
- `sender_id`, `channel`, `chat_id`
|
|
135
|
+
- "set" 时传:`display_name`, `line1`, `line2`, `line3`, `visible`, `matching_context`
|
|
136
|
+
|
|
137
|
+
名片内容:
|
|
138
|
+
- **display_name**:显示名称
|
|
139
|
+
- **line1**:个人描述(谁 / 做什么)
|
|
140
|
+
- **line2**:想认识的人
|
|
141
|
+
- **line3**:想要的交流方式
|
|
142
|
+
- **matching_context**(more_information,不展示给别人):agent 基于对用户的了解生成的详细描述,~200 字。**这是匹配的核心数据源。** line1/2/3 从它提炼出来,不是反过来。
|
|
173
143
|
|
|
174
144
|
### `antenna_accept`
|
|
175
|
-
|
|
176
|
-
- `sender_id`, `channel`, `
|
|
177
|
-
-
|
|
145
|
+
接受一个匹配。**不需要先 scan**——任何发现路径都可以触发 accept。
|
|
146
|
+
- `sender_id`, `channel`, `chat_id`:必填
|
|
147
|
+
- 三种方式指定对方(任选一种):
|
|
148
|
+
- `ref`:来自 scan/discover 结果的编号
|
|
149
|
+
- `profile_slug`:来自 profile 链接(如 `antenna.fyi/p/yi` → `profile_slug="yi"`)
|
|
150
|
+
- `target_device_id`:内部 ID(尽量用 ref 或 slug)
|
|
151
|
+
- `contact_info`(可选):分享联系方式
|
|
152
|
+
|
|
153
|
+
### `antenna_pass`
|
|
154
|
+
跳过一个人,不再推荐。
|
|
155
|
+
- `sender_id`, `channel`, `chat_id`
|
|
156
|
+
- `ref` 或 `target_device_id`
|
|
178
157
|
|
|
179
158
|
### `antenna_check_matches`
|
|
180
|
-
|
|
181
|
-
- `sender_id`, `channel`
|
|
182
|
-
-
|
|
159
|
+
检查匹配状态。
|
|
160
|
+
- `sender_id`, `channel`, `chat_id`
|
|
161
|
+
- 返回 mutual matches + incoming accepts + 联系方式
|
|
183
162
|
|
|
184
163
|
### `antenna_bind`
|
|
185
|
-
|
|
186
|
-
- `sender_id`, `channel
|
|
187
|
-
- `purpose
|
|
188
|
-
- `event_code
|
|
189
|
-
-
|
|
190
|
-
-
|
|
191
|
-
|
|
192
|
-
|
|
164
|
+
生成 GPS 绑定链接。
|
|
165
|
+
- `sender_id`, `channel`, `chat_id`
|
|
166
|
+
- `purpose`:`'profile'`(默认,更新用户位置)或 `'event'`(设活动位置)
|
|
167
|
+
- `event_code`:purpose=event 时必填
|
|
168
|
+
- 返回 URL,用户在手机打开后自动共享位置
|
|
169
|
+
- **Onboarding 后必须调用。** 不要等用户问。
|
|
170
|
+
|
|
171
|
+
### `antenna_link_account`
|
|
172
|
+
关联 agent profile 到 antenna.fyi 网站账号。
|
|
173
|
+
- `sender_id`, `channel`, `chat_id`:必填
|
|
174
|
+
- `user_id`:用户的 antenna.fyi 账号 UUID(从 dashboard 获取)
|
|
175
|
+
- 把已有的 agent profile(带全部历史)绑定到网站账号
|
|
176
|
+
- 如果用户先在网站注册了(产生空 profile),空 profile 自动删除
|
|
177
|
+
- 一次性操作
|
|
193
178
|
|
|
194
179
|
### `antenna_discover`
|
|
195
|
-
|
|
196
|
-
- `sender_id`, `channel
|
|
197
|
-
-
|
|
198
|
-
-
|
|
199
|
-
- Use this in the daily cron job, or when user asks "find someone interesting globally"
|
|
200
|
-
|
|
201
|
-
### `antenna_pass`
|
|
202
|
-
Pass/skip a person. They won't be recommended again.
|
|
203
|
-
- `sender_id`, `channel`: from context
|
|
204
|
-
- `ref`: ref number from scan/discover results (e.g. '1')
|
|
205
|
-
- `target_device_id`: device ID (use ref instead when possible)
|
|
206
|
-
- Use when the user says "skip", "pass", "not interested", etc.
|
|
180
|
+
全球推荐——每天 1 个最匹配的人。
|
|
181
|
+
- `sender_id`, `channel`, `chat_id`
|
|
182
|
+
- 不需要 GPS
|
|
183
|
+
- 如果所有人都推荐过了,返回"等新人加入"
|
|
207
184
|
|
|
208
185
|
### `antenna_checkin`
|
|
209
|
-
|
|
210
|
-
- `lat`, `lng
|
|
211
|
-
- `sender_id`, `channel
|
|
212
|
-
- `place_name
|
|
213
|
-
-
|
|
214
|
-
|
|
215
|
-
## Data Transparency - what Antenna sends
|
|
216
|
-
|
|
217
|
-
Antenna only communicates with Supabase (bcudjloikmpcqwcptuyd.supabase.co) via HTTPS.
|
|
218
|
-
|
|
219
|
-
**Data sent:**
|
|
220
|
-
- GPS coordinates (never stored raw — location is processed server-side)
|
|
221
|
-
- Your three-line profile card (text you wrote yourself)
|
|
222
|
-
- Match status (accept/skip)
|
|
223
|
-
- Contact info you choose to share
|
|
224
|
-
- Profile embedding vector (generated from your 3 lines, used for matching)
|
|
225
|
-
|
|
226
|
-
**Data NOT sent:**
|
|
227
|
-
- Your conversations with your agent
|
|
228
|
-
- Your files, browsing history, or any other personal data
|
|
229
|
-
- Anything not listed above
|
|
186
|
+
签到——更新你的位置。
|
|
187
|
+
- `lat`, `lng`:必填
|
|
188
|
+
- `sender_id`, `channel`, `chat_id`
|
|
189
|
+
- `place_name`:可选
|
|
190
|
+
- 用于"我在 XX"场景
|
|
230
191
|
|
|
231
|
-
|
|
232
|
-
Visibility is controlled by time decay — recent event participants are fully visible, older connections gradually fade.
|
|
233
|
-
Source code is open: https://github.com/H1an1/Antenna
|
|
234
|
-
|
|
235
|
-
## Behavior guidelines
|
|
236
|
-
|
|
237
|
-
### First-time user - 名片创建原则
|
|
238
|
-
|
|
239
|
-
具体流程见上方 Onboarding 第二步。以下是 agent 应该遵守的原则:
|
|
240
|
-
|
|
241
|
-
- **不要让用户填表。** 不要一次性说"请填写 emoji、名字、三句话介绍"--跟用户聊天,你来生成。
|
|
242
|
-
- **每次只问一个问题。** 不要一口气问完所有信息。
|
|
243
|
-
- **用户说的原话尽量保留。** 可以帮用户缩短太长的回答,但要在预览时让用户确认。
|
|
244
|
-
- **不要在名片里写联系方式。** 名片三句话对所有人可见。联系方式应该在 accept 时单独分享,只有双方都同意后才能看到。如果用户在聊天中提到联系方式,提醒他们。
|
|
245
|
-
- **line1 必填。** 后端会拒绝没有 line1 的新 profile,并对缺失的 line2/line3 返回 warning。
|
|
246
|
-
- **确认后才存。** 见 Onboarding 第二步。
|
|
192
|
+
## GPS Logic
|
|
247
193
|
|
|
248
|
-
|
|
194
|
+
**Profile GPS** — 用户的位置
|
|
195
|
+
- 通过 `antenna_bind(purpose="profile")` 或 `antenna_checkin` 更新
|
|
196
|
+
- 位置不原始存储
|
|
197
|
+
- 2 小时后概念上过期,agent 应提示刷新
|
|
249
198
|
|
|
250
|
-
|
|
199
|
+
**Event GPS** — 活动的位置
|
|
200
|
+
- 通过 `antenna_bind(purpose="event")` 或 `antenna_event_create(lat, lng)` 设置
|
|
201
|
+
- 精确坐标(不模糊)
|
|
202
|
+
- 不过期
|
|
251
203
|
|
|
252
|
-
|
|
204
|
+
## Behavior Guidelines
|
|
253
205
|
|
|
254
|
-
|
|
206
|
+
### 名片创建原则
|
|
207
|
+
- **不要让用户填表。** 跟用户聊天,你来生成。
|
|
208
|
+
- **每次只问一个问题。**
|
|
209
|
+
- **用户说的原话尽量保留。** 帮缩短但让用户确认。
|
|
210
|
+
- **不要在名片里写联系方式。** 联系方式在 accept 时分享。
|
|
211
|
+
- **line1 必填。**
|
|
212
|
+
- **确认后才存。**
|
|
255
213
|
|
|
256
|
-
|
|
214
|
+
### Showing results — 你来判断
|
|
257
215
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
216
|
+
scan 和 discover 返回的是 raw profile cards,**没有打分**。你需要:
|
|
217
|
+
1. 读每个人的名片 + more_information
|
|
218
|
+
2. 结合你对用户的全部了解判断谁值得推荐
|
|
219
|
+
3. 为每个推荐的人写一句**个性化的理由**
|
|
220
|
+
4. **不要推荐所有人。** 质量 > 数量。
|
|
261
221
|
|
|
262
|
-
|
|
222
|
+
**全球推荐 fallback:** 如果 scan 结果有 `global: true`,说明附近没人。告诉用户"附近暂时没人,但全球有这个人跟你很像"。
|
|
263
223
|
|
|
264
|
-
|
|
265
|
-
> 🎸 **小林** - 在组后摇乐队,找吉他手
|
|
266
|
-
> → 你不是最近在学吉他吗?这人在找吉他手诶
|
|
224
|
+
### Profile 链接场景
|
|
267
225
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
226
|
+
用户收到或提到 `antenna.fyi/p/xxx` 时:
|
|
227
|
+
1. 抓取页面,读 `#antenna-profile-data` JSON
|
|
228
|
+
2. 展示对方 profile + 你的判断
|
|
229
|
+
3. 用户想 accept → `antenna_accept(profile_slug="xxx")`
|
|
230
|
+
4. 用户想 skip → `antenna_pass` 或直接不操作
|
|
271
231
|
|
|
272
|
-
|
|
232
|
+
**这跟 scan 是完全平级的发现路径。**
|
|
273
233
|
|
|
274
234
|
### Accepting & contact exchange
|
|
275
|
-
|
|
276
|
-
1.
|
|
277
|
-
2.
|
|
278
|
-
3.
|
|
279
|
-
4.
|
|
280
|
-
5.
|
|
281
|
-
6.
|
|
282
|
-
|
|
283
|
-
**不要跳过第 2 步。** 联系方式是最终目标--不然 accept 了也没用,两个人找不到对方。
|
|
284
|
-
|
|
285
|
-
### Checking match status
|
|
286
|
-
Use `antenna_check_matches` when:
|
|
287
|
-
- User asks "有人回复我吗" / "匹配状态怎么样"
|
|
288
|
-
- Periodically during conversation if the user has pending matches
|
|
289
|
-
|
|
290
|
-
### Location sources
|
|
291
|
-
- **Telegram/WhatsApp location**: context will have `LocationLat`, `LocationLon` - use directly
|
|
292
|
-
- **User says a place name**: geocode it first (use web_search or a geocoding service), then call antenna_scan
|
|
293
|
-
- **Live location**: note that it's real-time, tell the user you'll check for new people
|
|
235
|
+
accept 可以从任何路径触发:
|
|
236
|
+
1. 调 `antenna_accept`(用 ref、profile_slug 或 device_id)
|
|
237
|
+
2. **立刻问**:"想分享什么联系方式给对方?"
|
|
238
|
+
3. 用户给了 → 再调一次 `antenna_accept` 带 `contact_info`
|
|
239
|
+
4. 用户不想 → "先 accept 着,以后想分享再说"
|
|
240
|
+
5. 如果 mutual match → 展示对方联系方式
|
|
241
|
+
6. 如果还没 mutual → "已发出,等对方回应"
|
|
294
242
|
|
|
295
|
-
|
|
296
|
-
- Never reveal exact coordinates to other users
|
|
297
|
-
- **Never show device_id to users** (e.g. `telegram:12345`, `discord:67890`) - this is internal only
|
|
298
|
-
- Never share someone's platform or username with another user
|
|
299
|
-
- Only show the profile info (name, emoji, three lines)
|
|
300
|
-
- Contact info is only shared when the user explicitly agrees
|
|
301
|
-
- Location is never stored raw
|
|
243
|
+
**不要跳过第 2 步。**
|
|
302
244
|
|
|
303
|
-
###
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
-
|
|
307
|
-
-
|
|
308
|
-
-
|
|
309
|
-
|
|
310
|
-
|
|
245
|
+
### Privacy
|
|
246
|
+
- **永远不要显示 device_id**——这是内部标识符
|
|
247
|
+
- 只展示名字 + 三句话 + 你写的匹配理由
|
|
248
|
+
- 不要泄露对方的平台或用户名
|
|
249
|
+
- 联系方式只在用户明确同意时分享
|
|
250
|
+
- GPS 不原始存储
|
|
251
|
+
|
|
252
|
+
### Time Decay — 可见性衰减
|
|
253
|
+
- Event 后 0-7 天:全部参与者互相可见
|
|
254
|
+
- 7-30 天:只有互相 scan 过 / 有共同活动的人可见
|
|
255
|
+
- 30 天后:需要新事件激活
|
|
256
|
+
|
|
257
|
+
### Heartbeat — 自动查匹配
|
|
258
|
+
Plugin 后台每 10 分钟查一次新匹配。看到 `[Antenna] 🎉` 时:
|
|
259
|
+
1. 调 `antenna_check_matches`
|
|
260
|
+
2. 告诉用户 + 展示对方名片
|
|
261
|
+
3. 展示联系方式(如果有)
|
|
311
262
|
|
|
312
|
-
|
|
263
|
+
## Events
|
|
313
264
|
|
|
314
|
-
|
|
265
|
+
详见 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`。
|
|
315
266
|
|
|
316
|
-
|
|
317
|
-
1. 调 `antenna_check_matches` 拿详情
|
|
318
|
-
2. 告诉用户:"有人想认识你!" + 展示对方名片
|
|
319
|
-
3. 如果对方分享了联系方式,一并展示
|
|
267
|
+
## Data Transparency
|
|
320
268
|
|
|
321
|
-
|
|
269
|
+
Antenna 只跟 Supabase (bcudjloikmpcqwcptuyd.supabase.co) 通信。
|
|
322
270
|
|
|
323
|
-
|
|
271
|
+
**发送的数据:** GPS(不原始存储)、名片文本、匹配状态、你选择分享的联系方式、Profile embedding。
|
|
272
|
+
**不发送的数据:** 你跟 agent 的对话、文件、浏览记录。
|
|
324
273
|
|
|
325
|
-
|
|
274
|
+
Source code: https://github.com/H1an1/Antenna
|