antenna-fyi 1.3.22 → 1.3.24
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 +23 -1
- package/lib/core.js +75 -0
- package/lib/hermes-plugin/schemas.py +18 -0
- package/lib/hermes-plugin/tools.py +78 -0
- package/lib/mcp.js +26 -0
- package/package.json +1 -1
- package/skill/SKILL.md +117 -102
package/bin/antenna.js
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
handleSetup,
|
|
16
16
|
handleConfig,
|
|
17
17
|
handleStatus,
|
|
18
|
+
handleLinkAccount,
|
|
18
19
|
handleInstallSkill,
|
|
19
20
|
handleInstallPlugin,
|
|
20
21
|
handleInstallHermesPlugin,
|
|
@@ -56,6 +57,8 @@ async function main() {
|
|
|
56
57
|
return handleConfig(f);
|
|
57
58
|
case "status":
|
|
58
59
|
return handleStatus(f);
|
|
60
|
+
case "link-account":
|
|
61
|
+
return handleLinkAccount(f);
|
|
59
62
|
case "install-skill":
|
|
60
63
|
return handleInstallSkill();
|
|
61
64
|
case "install-plugin":
|
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 } 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 } 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";
|
|
@@ -401,9 +401,11 @@ export async function handleConfig(f) {
|
|
|
401
401
|
const config = loadConfig();
|
|
402
402
|
config.key = f.key;
|
|
403
403
|
config.device_id = result.device_id;
|
|
404
|
+
config.user_id = result.user_id;
|
|
404
405
|
config.display_name = result.display_name;
|
|
405
406
|
saveConfig(config);
|
|
406
407
|
console.log(`\n✅ Authenticated as ${result.display_name || 'Antenna user'}`);
|
|
408
|
+
console.log(` User ID: ${result.user_id}`);
|
|
407
409
|
console.log(` Device ID: ${result.device_id}`);
|
|
408
410
|
console.log(` Config saved to ~/.antenna/config.json\n`);
|
|
409
411
|
} catch (e) {
|
|
@@ -425,6 +427,26 @@ export async function handleConfig(f) {
|
|
|
425
427
|
}
|
|
426
428
|
}
|
|
427
429
|
|
|
430
|
+
export async function handleLinkAccount(f) {
|
|
431
|
+
const id = resolveId(f);
|
|
432
|
+
const config = loadConfig();
|
|
433
|
+
const userId = f['user-id'] || f.userId || config.user_id;
|
|
434
|
+
if (!id) return console.error("Usage: antenna link-account --id <platform>:<user_id> --user-id <antenna.fyi UUID>\n Or: antenna link-account (uses config device_id + user_id)");
|
|
435
|
+
if (!userId) return console.error("❌ No user_id. Run 'antenna config --key <your-key>' first, or pass --user-id <UUID>.");
|
|
436
|
+
try {
|
|
437
|
+
const result = await linkAccount({ device_id: id, user_id: userId });
|
|
438
|
+
if (result.error) {
|
|
439
|
+
console.error(`\n❌ ${result.message || result.error}`);
|
|
440
|
+
} else {
|
|
441
|
+
console.log(`\n✅ ${result.message || 'Account linked.'}`);
|
|
442
|
+
console.log(` Device ID: ${id}`);
|
|
443
|
+
console.log(` User ID: ${userId}\n`);
|
|
444
|
+
}
|
|
445
|
+
} catch (e) {
|
|
446
|
+
console.error(`\n❌ Failed: ${e.message}`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
428
450
|
export async function handleStatus(f) {
|
|
429
451
|
const supabaseUrl = process.env.ANTENNA_SUPABASE_URL || process.env.ANTENNA_URL || "https://bcudjloikmpcqwcptuyd.supabase.co";
|
|
430
452
|
console.log("\n📡 Antenna Status\n");
|
package/lib/core.js
CHANGED
|
@@ -558,6 +558,81 @@ export async function discover({ device_id, supabaseUrl, supabaseKey }) {
|
|
|
558
558
|
};
|
|
559
559
|
}
|
|
560
560
|
|
|
561
|
+
// ─── initialRecommendations (one-time first-use) ──────────────────────
|
|
562
|
+
|
|
563
|
+
export async function initialRecommendations({ device_id, supabaseUrl, supabaseKey }) {
|
|
564
|
+
const sb = getClient(supabaseUrl, supabaseKey);
|
|
565
|
+
|
|
566
|
+
const { data: results, error } = await sb.rpc("initial_recommendations", {
|
|
567
|
+
p_device_id: device_id,
|
|
568
|
+
p_limit: 3,
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
if (error) throw new Error(error.message);
|
|
572
|
+
|
|
573
|
+
if (!results || results.length === 0) {
|
|
574
|
+
return {
|
|
575
|
+
count: 0,
|
|
576
|
+
profiles: [],
|
|
577
|
+
initial: true,
|
|
578
|
+
message: "暂时没有推荐,等有更多人加入!",
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Build ref map + generate match reasons
|
|
583
|
+
const _refMap = {};
|
|
584
|
+
const myProfile = await getProfile({ device_id, supabaseUrl, supabaseKey });
|
|
585
|
+
const myLines = myProfile ? [myProfile.line1, myProfile.line2, myProfile.line3].filter(Boolean).join(". ") : "";
|
|
586
|
+
|
|
587
|
+
const profiles = [];
|
|
588
|
+
for (let i = 0; i < results.length; i++) {
|
|
589
|
+
const p = results[i];
|
|
590
|
+
const ref = String(i + 1);
|
|
591
|
+
_refMap[ref] = p.device_id;
|
|
592
|
+
|
|
593
|
+
const theirLines = [p.line1, p.line2, p.line3].filter(Boolean).join(". ");
|
|
594
|
+
let reason = null;
|
|
595
|
+
if (myLines && theirLines) {
|
|
596
|
+
reason = await generateMatchReason(myLines, theirLines);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
profiles.push({
|
|
600
|
+
ref,
|
|
601
|
+
name: p.display_name || "匿名",
|
|
602
|
+
emoji: p.emoji || "👤",
|
|
603
|
+
personal_description: p.line1,
|
|
604
|
+
looking_for: p.line2,
|
|
605
|
+
conversation_style: p.line3,
|
|
606
|
+
more_information: p.matching_context || null,
|
|
607
|
+
profile_slug: p.profile_slug || null,
|
|
608
|
+
match_reason: reason,
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Log who was recommended (for dedup in daily discover)
|
|
613
|
+
for (const p of results) {
|
|
614
|
+
await sb.rpc("log_recommendation", {
|
|
615
|
+
p_device_id: device_id,
|
|
616
|
+
p_recommended_id: p.device_id,
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Persist ref map to DB
|
|
621
|
+
if (device_id && Object.keys(_refMap).length > 0) {
|
|
622
|
+
try {
|
|
623
|
+
await sb.rpc("save_scan_refs", { p_owner: device_id, p_refs: _refMap });
|
|
624
|
+
} catch { /* best effort */ }
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
return {
|
|
628
|
+
count: profiles.length,
|
|
629
|
+
profiles,
|
|
630
|
+
_ref_map: _refMap,
|
|
631
|
+
initial: true,
|
|
632
|
+
message: "这是你的首次推荐——基于你的名片,这几个人跟你最匹配。",
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
|
|
561
636
|
// ─── pass ───────────────────────────────────────────────────────────
|
|
562
637
|
|
|
563
638
|
export async function pass({ device_id, target_device_id, ref, supabaseUrl, supabaseKey }) {
|
|
@@ -402,3 +402,21 @@ LINK_ACCOUNT_SCHEMA = {
|
|
|
402
402
|
"required": ["sender_id", "channel", "chat_id", "user_id"],
|
|
403
403
|
},
|
|
404
404
|
}
|
|
405
|
+
|
|
406
|
+
INITIAL_RECOMMENDATIONS_SCHEMA = {
|
|
407
|
+
"name": "antenna_initial_recommendations",
|
|
408
|
+
"description": (
|
|
409
|
+
"Get initial recommendations for a new user \u2014 2-3 people most similar to them. "
|
|
410
|
+
"One-time only, does NOT consume daily discover quota. "
|
|
411
|
+
"Use right after profile creation in onboarding."
|
|
412
|
+
),
|
|
413
|
+
"parameters": {
|
|
414
|
+
"type": "object",
|
|
415
|
+
"properties": {
|
|
416
|
+
"sender_id": {"type": "string", "description": "The sender's user ID"},
|
|
417
|
+
"channel": {"type": "string", "description": "Platform name (any platform works)"},
|
|
418
|
+
"chat_id": {"type": "string", "description": "REQUIRED for notifications. Pass chat/channel ID from message context."},
|
|
419
|
+
},
|
|
420
|
+
"required": ["sender_id", "channel", "chat_id"],
|
|
421
|
+
},
|
|
422
|
+
}
|
|
@@ -735,3 +735,81 @@ def handle_link_account(params: dict) -> str:
|
|
|
735
735
|
data = resp.data or {}
|
|
736
736
|
data["message"] = "账号已关联!现在你可以在 antenna.fyi/me 看到你的完整 profile 和匹配记录了。"
|
|
737
737
|
return _ok(data)
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
def handle_initial_recommendations(params: dict) -> str:
|
|
741
|
+
"""Get initial recommendations for a new user (2-3 people). One-time only."""
|
|
742
|
+
sb = _sb()
|
|
743
|
+
did = _device_id(params["sender_id"], params["channel"], params.get("chat_id"))
|
|
744
|
+
|
|
745
|
+
resp = sb.rpc("initial_recommendations", {
|
|
746
|
+
"p_device_id": did,
|
|
747
|
+
"p_limit": 3,
|
|
748
|
+
}).execute()
|
|
749
|
+
results = resp.data or []
|
|
750
|
+
|
|
751
|
+
if not results:
|
|
752
|
+
return _ok({"count": 0, "profiles": [], "initial": True, "message": "暂时没有推荐,等有更多人加入!"})
|
|
753
|
+
|
|
754
|
+
global _last_ref_map
|
|
755
|
+
_last_ref_map = {}
|
|
756
|
+
profiles = []
|
|
757
|
+
|
|
758
|
+
# Get my profile for match reason
|
|
759
|
+
my_prof = sb.rpc("get_profile", {"p_device_id": did}).execute()
|
|
760
|
+
my_data = my_prof.data or {}
|
|
761
|
+
my_lines = [my_data.get("line1", ""), my_data.get("line2", ""), my_data.get("line3", "")]
|
|
762
|
+
|
|
763
|
+
ref_map = {}
|
|
764
|
+
for i, p in enumerate(results):
|
|
765
|
+
ref = str(i + 1)
|
|
766
|
+
_last_ref_map[ref] = p.get("device_id")
|
|
767
|
+
ref_map[ref] = p.get("device_id")
|
|
768
|
+
|
|
769
|
+
their_lines = [p.get("line1", ""), p.get("line2", ""), p.get("line3", "")]
|
|
770
|
+
|
|
771
|
+
# Generate match reason via Edge Function
|
|
772
|
+
match_reason = None
|
|
773
|
+
try:
|
|
774
|
+
req = urllib.request.Request(
|
|
775
|
+
f"{BUILTIN_URL}/functions/v1/generate-match-reason",
|
|
776
|
+
data=json.dumps({"my_lines": my_lines, "their_lines": their_lines}).encode(),
|
|
777
|
+
headers={"Content-Type": "application/json", "Authorization": f"Bearer {BUILTIN_KEY}"},
|
|
778
|
+
)
|
|
779
|
+
res = urllib.request.urlopen(req, timeout=10)
|
|
780
|
+
body = json.loads(res.read().decode())
|
|
781
|
+
match_reason = body.get("reason")
|
|
782
|
+
except Exception:
|
|
783
|
+
pass
|
|
784
|
+
|
|
785
|
+
profile = {
|
|
786
|
+
"ref": ref,
|
|
787
|
+
"emoji": p.get("emoji") or "\ud83d\udc64",
|
|
788
|
+
"name": p.get("display_name") or "匿名",
|
|
789
|
+
"personal_description": p.get("line1"),
|
|
790
|
+
"looking_for": p.get("line2"),
|
|
791
|
+
"conversation_style": p.get("line3"),
|
|
792
|
+
"more_information": p.get("matching_context") or None,
|
|
793
|
+
"profile_slug": p.get("profile_slug") or None,
|
|
794
|
+
}
|
|
795
|
+
if match_reason:
|
|
796
|
+
profile["match_reason"] = match_reason
|
|
797
|
+
profiles.append(profile)
|
|
798
|
+
|
|
799
|
+
# Save refs and log recommendations
|
|
800
|
+
try:
|
|
801
|
+
sb.rpc("save_scan_refs", {"p_owner": did, "p_refs": ref_map}).execute()
|
|
802
|
+
except Exception:
|
|
803
|
+
pass
|
|
804
|
+
for p in results:
|
|
805
|
+
try:
|
|
806
|
+
sb.rpc("log_recommendation", {"p_device_id": did, "p_recommended_id": p["device_id"]}).execute()
|
|
807
|
+
except Exception:
|
|
808
|
+
pass
|
|
809
|
+
|
|
810
|
+
return _ok({
|
|
811
|
+
"count": len(profiles),
|
|
812
|
+
"profiles": profiles,
|
|
813
|
+
"initial": True,
|
|
814
|
+
"message": "这是你的首次推荐——基于你的名片,这几个人跟你最匹配。",
|
|
815
|
+
})
|
package/lib/mcp.js
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
checkin,
|
|
14
14
|
createBindToken,
|
|
15
15
|
discover,
|
|
16
|
+
initialRecommendations,
|
|
16
17
|
createEvent,
|
|
17
18
|
endEvent,
|
|
18
19
|
eventCheckin,
|
|
@@ -258,6 +259,31 @@ export async function startMcpServer() {
|
|
|
258
259
|
}
|
|
259
260
|
);
|
|
260
261
|
|
|
262
|
+
// ─── antenna_initial_recommendations ─────────────────────────
|
|
263
|
+
|
|
264
|
+
server.tool(
|
|
265
|
+
"antenna_initial_recommendations",
|
|
266
|
+
"Get initial recommendations for a new user — 2-3 people most similar to them. One-time only, does NOT consume daily discover quota. Use right after profile creation in onboarding.",
|
|
267
|
+
{
|
|
268
|
+
sender_id: z.string().describe("The sender's user ID"),
|
|
269
|
+
channel: z.string().describe("Channel name"),
|
|
270
|
+
chat_id: z.string().optional().describe("Chat/channel ID for notifications"),
|
|
271
|
+
},
|
|
272
|
+
async ({ sender_id, channel, chat_id }) => {
|
|
273
|
+
try {
|
|
274
|
+
const result = await initialRecommendations({ device_id: deriveDeviceId(sender_id, channel) });
|
|
275
|
+
if (result._ref_map) {
|
|
276
|
+
_lastRefMap = result._ref_map;
|
|
277
|
+
const { _ref_map, ...clean } = result;
|
|
278
|
+
return jsonResult(clean);
|
|
279
|
+
}
|
|
280
|
+
return jsonResult(result);
|
|
281
|
+
} catch (e) {
|
|
282
|
+
return jsonResult({ error: e.message });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
);
|
|
286
|
+
|
|
261
287
|
// ─── antenna_pass ────────────────────────────────────────────
|
|
262
288
|
|
|
263
289
|
server.tool(
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: antenna
|
|
3
|
-
description: "AI-native social discovery via Antenna. Use when a user wants to meet interesting people
|
|
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
|
-
Antenna 帮你的用户发现值得认识的人。不只是"附近的人"
|
|
9
|
+
Antenna 帮你的用户发现值得认识的人。不只是"附近的人"--profile link、全球推荐、活动,都是发现路径。
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
**核心流程:发现 → 判断 → accept → 交换联系方式。**
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
- 📡 **附近扫描**
|
|
15
|
-
- 🌍 **全球推荐**
|
|
16
|
-
- 🔗 **Profile 链接**
|
|
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
|
-
**Accept 不需要先 scan。** 只要你知道对方的 `ref`、`profile_slug` 或 `device_id
|
|
19
|
+
**Accept 不需要先 scan。** 只要你知道对方的 `ref`、`profile_slug` 或 `device_id`,就可以直接 accept。
|
|
20
20
|
|
|
21
21
|
## How to call Antenna
|
|
22
22
|
|
|
23
|
-
- 如果有 Antenna plugin tools
|
|
24
|
-
-
|
|
25
|
-
- **所有 tool 必须传 `chat_id
|
|
23
|
+
- 如果有 Antenna plugin tools(antenna_scan, antenna_profile 等),直接用。
|
|
24
|
+
- 如果没有,用 CLI:`antenna <command> [flags]`。
|
|
25
|
+
- **所有 tool 必须传 `chat_id`**--Antenna 用它发通知。
|
|
26
26
|
|
|
27
27
|
## Onboarding — 装上后主动引导
|
|
28
28
|
|
|
@@ -32,9 +32,13 @@ Plugin 安装后,agent **主动**开始引导,不要等用户问。
|
|
|
32
32
|
|
|
33
33
|
### 流程
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
**第一步:拿到 API key → 配置**
|
|
36
36
|
> "嘿,你装了 Antenna——它能帮你发现有意思的人。先确认一下,你在 antenna.fyi 注册过了吗?拿到 API key 了吗?"
|
|
37
37
|
|
|
38
|
+
用户给了 API key 后,调 `antenna config --key <key>` 验证。这会返回 `user_id` 和 `device_id`。
|
|
39
|
+
|
|
40
|
+
**⚠️ 之后所有操作必须用 API key 验证返回的 device_id(格式 `user:xxx`)。不要自己拼 `channel:sender_id`。** 这样 agent 创建的 profile 才能在 dashboard 上显示。
|
|
41
|
+
|
|
38
42
|
**第二步:聊天收集 → 生成名片 → 确认**
|
|
39
43
|
|
|
40
44
|
跟用户聊几句,了解他们是谁、做什么、想认识什么人。然后 agent 自己生成:
|
|
@@ -50,108 +54,112 @@ Plugin 安装后,agent **主动**开始引导,不要等用户问。
|
|
|
50
54
|
>
|
|
51
55
|
> 这样可以吗?要改哪里告诉我。
|
|
52
56
|
|
|
53
|
-
|
|
57
|
+
确认后用 config 里的 device_id 调 `antenna_profile(action="set")` 保存。**不要跳过确认。**
|
|
58
|
+
|
|
59
|
+
**⚠️ sender_id 用 config 里的 device_id,不要用 channel:sender_id。**
|
|
54
60
|
|
|
55
|
-
|
|
61
|
+
**第三步:立刻推荐 2-3 个人**
|
|
56
62
|
|
|
57
|
-
|
|
58
|
-
> "
|
|
63
|
+
名片存好后,**立刻**调 `antenna_initial_recommendations` 获取首次推荐:
|
|
64
|
+
> "名片存好了!我先帮你看看有谁跟你特别像--"
|
|
59
65
|
|
|
60
|
-
|
|
66
|
+
这是一次性的首次推荐(不消耗每日 discover 额度)。如果返回空,说明还没有足够的用户--跳过这步,继续 antenna_bind。
|
|
61
67
|
|
|
62
|
-
|
|
63
|
-
> "点这个链接,在手机浏览器打开,允许定位就行:[链接]"
|
|
68
|
+
**第四步:调 antenna_bind 获取 GPS 链接**
|
|
64
69
|
|
|
65
|
-
|
|
70
|
+
**强制。** 名片存好后**必须立刻**调用 `antenna_bind` 生成链接并发给用户:
|
|
71
|
+
> "点这个链接,在手机浏览器打开,允许定位就行:[链接]"
|
|
66
72
|
|
|
67
|
-
|
|
73
|
+
GPS 不是 Antenna 的唯一入口,但它开启了附近发现的能力。
|
|
68
74
|
|
|
69
|
-
|
|
75
|
+
**第五步:设置定时任务**
|
|
76
|
+
|
|
77
|
+
设一个每小时检查匹配的 cron:
|
|
70
78
|
```
|
|
71
79
|
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."
|
|
72
80
|
```
|
|
73
81
|
|
|
74
|
-
|
|
75
|
-
> "
|
|
82
|
+
**第六步:问要不要全球推荐**
|
|
83
|
+
> "要我每天帮你找一个全球最像你的人吗?"
|
|
76
84
|
|
|
77
85
|
### Linking to antenna.fyi account
|
|
78
86
|
|
|
79
|
-
如果用户之前通过 agent 创建过 profile
|
|
87
|
+
如果用户之前通过 agent 创建过 profile(没有网站账号),现在注册了 antenna.fyi:
|
|
80
88
|
1. 让用户从 antenna.fyi/me 复制 user ID
|
|
81
89
|
2. 调 `antenna_link_account(user_id = "xxx")`
|
|
82
|
-
3.
|
|
90
|
+
3. 确认:"关联成功!你现在可以在 dashboard 上看到完整的 profile 和匹配记录了。"
|
|
83
91
|
|
|
84
|
-
这把 agent 创建的 profile
|
|
92
|
+
这把 agent 创建的 profile(带全部历史)关联到网站账号。
|
|
85
93
|
|
|
86
94
|
## When to use
|
|
87
95
|
|
|
88
|
-
-
|
|
96
|
+
- **首次安装后**:主动 onboarding
|
|
89
97
|
- 用户分享位置 → `antenna_scan`
|
|
90
98
|
- 用户问"附近有谁" → `antenna_scan`
|
|
91
|
-
- 用户收到 profile
|
|
99
|
+
- 用户收到 profile 链接(`antenna.fyi/p/xxx`)→ 读取 profile → 判断 → `antenna_accept`
|
|
92
100
|
- 用户想编辑名片 → `antenna_profile`
|
|
93
101
|
- 用户说 accept / skip → `antenna_accept` / `antenna_pass`
|
|
94
102
|
- 用户问匹配状态 → `antenna_check_matches`
|
|
95
103
|
|
|
96
104
|
## Discovery Paths
|
|
97
105
|
|
|
98
|
-
### 1.
|
|
106
|
+
### 1. 附近扫描(antenna_scan)
|
|
99
107
|
基于 GPS 发现 1km 内的人。需要位置信息。
|
|
100
|
-
- 如果没有 GPS
|
|
101
|
-
- 返回 raw profile cards
|
|
108
|
+
- 如果没有 GPS,先调 `antenna_bind` 或让用户发位置
|
|
109
|
+
- 返回 raw profile cards,**agent 判断谁值得推荐**
|
|
102
110
|
|
|
103
|
-
### 2.
|
|
104
|
-
每天 1
|
|
105
|
-
- 用在日常 cron
|
|
111
|
+
### 2. 全球推荐(antenna_discover)
|
|
112
|
+
每天 1 个全球最匹配的人,不需要 GPS。
|
|
113
|
+
- 用在日常 cron 里,或用户主动要求
|
|
106
114
|
|
|
107
115
|
### 3. Profile 链接
|
|
108
|
-
用户收到 `antenna.fyi/p/xxx`
|
|
109
|
-
1. 用 `web_fetch`
|
|
116
|
+
用户收到 `antenna.fyi/p/xxx` 链接时:
|
|
117
|
+
1. 用 `web_fetch` 读取页面--页面里有 `<script id="antenna-profile-data">` JSON,包含完整 profile
|
|
110
118
|
2. 读取 more_information、interest_tags、个人描述等
|
|
111
|
-
3.
|
|
119
|
+
3. 结合你对用户的了解,判断是否推荐
|
|
112
120
|
4. 如果用户想 accept → 调 `antenna_accept(profile_slug="xxx")`
|
|
113
121
|
|
|
114
122
|
**不需要先 scan。** Profile 链接是独立的发现路径。
|
|
115
123
|
|
|
116
|
-
### 4.
|
|
124
|
+
### 4. 活动(Events)
|
|
117
125
|
同一个活动里的人。详见 EVENTS.md。
|
|
118
126
|
|
|
119
127
|
## Tools
|
|
120
128
|
|
|
121
129
|
### `antenna_scan`
|
|
122
|
-
|
|
123
|
-
- `lat`, `lng
|
|
124
|
-
- `radius_m
|
|
125
|
-
- `sender_id`, `channel`, `chat_id
|
|
126
|
-
- 返回 `profiles
|
|
127
|
-
- 每个 profile 包含 `ref
|
|
130
|
+
扫描附近的人和活动。**只读--不会更新你的位置。**
|
|
131
|
+
- `lat`, `lng`:坐标(可选,没有的话用已绑定的 GPS)
|
|
132
|
+
- `radius_m`:搜索半径(默认 500m,最大 1000m)
|
|
133
|
+
- `sender_id`, `channel`, `chat_id`:必填
|
|
134
|
+
- 返回 `profiles`(附近的人)+ `nearby_events`(5km 内的活动)
|
|
135
|
+
- 每个 profile 包含 `ref`(用于 accept)、`profile_slug`(公开链接)、`more_information`(匹配上下文)
|
|
128
136
|
|
|
129
|
-
**GPS
|
|
137
|
+
**GPS 时效:** 如果 `last_seen_at` 超过 2 小时,提示用户更新位置。
|
|
130
138
|
|
|
131
139
|
### `antenna_profile`
|
|
132
140
|
查看或更新用户名片。
|
|
133
|
-
- `action
|
|
141
|
+
- `action`:"get" 或 "set"
|
|
134
142
|
- `sender_id`, `channel`, `chat_id`
|
|
135
|
-
- "set"
|
|
143
|
+
- "set" 时传:`display_name`, `line1`, `line2`, `line3`, `visible`, `matching_context`
|
|
136
144
|
|
|
137
|
-
|
|
138
|
-
- **display_name
|
|
139
|
-
- **line1
|
|
140
|
-
- **line2
|
|
141
|
-
- **line3
|
|
142
|
-
- **matching_context
|
|
145
|
+
名片内容:
|
|
146
|
+
- **display_name**:显示名称
|
|
147
|
+
- **line1**:个人描述(谁 / 做什么)
|
|
148
|
+
- **line2**:想认识的人
|
|
149
|
+
- **line3**:想要的交流方式
|
|
150
|
+
- **matching_context**(more_information,不展示给别人):agent 基于对用户的了解生成的详细描述,~200 字。**这是匹配的核心数据源。** line1/2/3 从它提炼出来,不是反过来。
|
|
143
151
|
|
|
144
152
|
### `antenna_accept`
|
|
145
|
-
接受一个匹配。**不需要先 scan
|
|
146
|
-
- `sender_id`, `channel`, `chat_id
|
|
147
|
-
-
|
|
148
|
-
- `ref
|
|
149
|
-
- `profile_slug
|
|
150
|
-
- `target_device_id
|
|
151
|
-
- `contact_info
|
|
153
|
+
接受一个匹配。**不需要先 scan**--任何发现路径都可以触发 accept。
|
|
154
|
+
- `sender_id`, `channel`, `chat_id`:必填
|
|
155
|
+
- 三种方式指定对方(任选一种):
|
|
156
|
+
- `ref`:来自 scan/discover 结果的编号
|
|
157
|
+
- `profile_slug`:来自 profile 链接(如 `antenna.fyi/p/yi` → `profile_slug="yi"`)
|
|
158
|
+
- `target_device_id`:内部 ID(尽量用 ref 或 slug)
|
|
159
|
+
- `contact_info`(可选):分享联系方式
|
|
152
160
|
|
|
153
161
|
### `antenna_pass`
|
|
154
|
-
|
|
162
|
+
跳过一个人,不再推荐。
|
|
155
163
|
- `sender_id`, `channel`, `chat_id`
|
|
156
164
|
- `ref` 或 `target_device_id`
|
|
157
165
|
|
|
@@ -163,68 +171,75 @@ openclaw cron add --every 1h --message "Check antenna matches: call antenna_chec
|
|
|
163
171
|
### `antenna_bind`
|
|
164
172
|
生成 GPS 绑定链接。
|
|
165
173
|
- `sender_id`, `channel`, `chat_id`
|
|
166
|
-
- `purpose
|
|
167
|
-
- `event_code
|
|
168
|
-
- 返回 URL
|
|
174
|
+
- `purpose`:`'profile'`(默认,更新用户位置)或 `'event'`(设活动位置)
|
|
175
|
+
- `event_code`:purpose=event 时必填
|
|
176
|
+
- 返回 URL,用户在手机打开后自动共享位置
|
|
169
177
|
- **Onboarding 后必须调用。** 不要等用户问。
|
|
170
178
|
|
|
171
179
|
### `antenna_link_account`
|
|
172
180
|
关联 agent profile 到 antenna.fyi 网站账号。
|
|
173
|
-
- `sender_id`, `channel`, `chat_id
|
|
174
|
-
- `user_id
|
|
175
|
-
- 把已有的 agent profile
|
|
176
|
-
-
|
|
181
|
+
- `sender_id`, `channel`, `chat_id`:必填
|
|
182
|
+
- `user_id`:用户的 antenna.fyi 账号 UUID(从 dashboard 获取)
|
|
183
|
+
- 把已有的 agent profile(带全部历史)绑定到网站账号
|
|
184
|
+
- 如果用户先在网站注册了(产生空 profile),空 profile 自动删除
|
|
177
185
|
- 一次性操作
|
|
178
186
|
|
|
179
187
|
### `antenna_discover`
|
|
180
|
-
|
|
188
|
+
全球推荐--每天 1 个最匹配的人。
|
|
181
189
|
- `sender_id`, `channel`, `chat_id`
|
|
182
190
|
- 不需要 GPS
|
|
183
|
-
-
|
|
191
|
+
- 如果所有人都推荐过了,返回"等新人加入"
|
|
192
|
+
|
|
193
|
+
### `antenna_initial_recommendations`
|
|
194
|
+
首次推荐--注册后立刻看到 2-3 个最匹配的人。
|
|
195
|
+
- `sender_id`, `channel`, `chat_id`: from context
|
|
196
|
+
- One-time only - second call returns empty
|
|
197
|
+
- Does NOT consume daily discover quota
|
|
198
|
+
- Use in onboarding step 3, right after profile save
|
|
184
199
|
|
|
185
200
|
### `antenna_checkin`
|
|
186
|
-
|
|
187
|
-
- `lat`, `lng
|
|
201
|
+
签到--更新你的位置。
|
|
202
|
+
- `lat`, `lng`:必填
|
|
188
203
|
- `sender_id`, `channel`, `chat_id`
|
|
189
|
-
- `place_name
|
|
204
|
+
- `place_name`:可选
|
|
190
205
|
- 用于"我在 XX"场景
|
|
191
206
|
|
|
192
207
|
## GPS Logic
|
|
193
208
|
|
|
194
|
-
**Profile GPS**
|
|
209
|
+
**Profile GPS** - 用户的位置
|
|
195
210
|
- 通过 `antenna_bind(purpose="profile")` 或 `antenna_checkin` 更新
|
|
196
211
|
- 位置不原始存储
|
|
197
|
-
- 2
|
|
212
|
+
- 2 小时后概念上过期,agent 应提示刷新
|
|
198
213
|
|
|
199
|
-
**Event GPS**
|
|
214
|
+
**Event GPS** - 活动的位置
|
|
200
215
|
- 通过 `antenna_bind(purpose="event")` 或 `antenna_event_create(lat, lng)` 设置
|
|
201
|
-
-
|
|
216
|
+
- 精确坐标(不模糊)
|
|
202
217
|
- 不过期
|
|
203
218
|
|
|
204
219
|
## Behavior Guidelines
|
|
205
220
|
|
|
206
221
|
### 名片创建原则
|
|
207
|
-
- **不要让用户填表。**
|
|
222
|
+
- **不要让用户填表。** 跟用户聊天,你来生成。
|
|
208
223
|
- **每次只问一个问题。**
|
|
209
224
|
- **用户说的原话尽量保留。** 帮缩短但让用户确认。
|
|
210
225
|
- **不要在名片里写联系方式。** 联系方式在 accept 时分享。
|
|
211
226
|
- **line1 必填。**
|
|
212
227
|
- **确认后才存。**
|
|
213
228
|
|
|
214
|
-
### Showing results
|
|
229
|
+
### Showing results - 你来判断
|
|
215
230
|
|
|
216
|
-
scan 和 discover 返回的是 raw profile cards
|
|
231
|
+
scan 和 discover 返回的是 raw profile cards,**没有打分**。你需要:
|
|
217
232
|
1. 读每个人的名片 + more_information
|
|
218
233
|
2. 结合你对用户的全部了解判断谁值得推荐
|
|
219
234
|
3. 为每个推荐的人写一句**个性化的理由**
|
|
220
235
|
4. **不要推荐所有人。** 质量 > 数量。
|
|
221
236
|
|
|
222
|
-
**全球推荐 fallback
|
|
237
|
+
**全球推荐 fallback:** 如果 scan 结果有 `global: true`,说明附近没人。告诉用户"附近暂时没人,但全球有这个人跟你很像"。
|
|
223
238
|
|
|
224
239
|
### Profile 链接场景
|
|
225
240
|
|
|
226
|
-
用户收到或提到 `antenna.fyi/p/xxx`
|
|
227
|
-
1.
|
|
241
|
+
用户收到或提到 `antenna.fyi/p/xxx` 时:
|
|
242
|
+
1. 抓取页面,读 `#antenna-profile-data` JSON
|
|
228
243
|
2. 展示对方 profile + 你的判断
|
|
229
244
|
3. 用户想 accept → `antenna_accept(profile_slug="xxx")`
|
|
230
245
|
4. 用户想 skip → `antenna_pass` 或直接不操作
|
|
@@ -232,43 +247,43 @@ scan 和 discover 返回的是 raw profile cards,**没有打分**。你需要
|
|
|
232
247
|
**这跟 scan 是完全平级的发现路径。**
|
|
233
248
|
|
|
234
249
|
### Accepting & contact exchange
|
|
235
|
-
accept
|
|
236
|
-
1. 调 `antenna_accept
|
|
237
|
-
2.
|
|
250
|
+
accept 可以从任何路径触发:
|
|
251
|
+
1. 调 `antenna_accept`(用 ref、profile_slug 或 device_id)
|
|
252
|
+
2. **立刻问**:"想分享什么联系方式给对方?"
|
|
238
253
|
3. 用户给了 → 再调一次 `antenna_accept` 带 `contact_info`
|
|
239
|
-
4. 用户不想 → "先 accept
|
|
254
|
+
4. 用户不想 → "先 accept 着,以后想分享再说"
|
|
240
255
|
5. 如果 mutual match → 展示对方联系方式
|
|
241
|
-
6. 如果还没 mutual → "
|
|
256
|
+
6. 如果还没 mutual → "已发出,等对方回应"
|
|
242
257
|
|
|
243
258
|
**不要跳过第 2 步。**
|
|
244
259
|
|
|
245
260
|
### Privacy
|
|
246
|
-
- **永远不要显示 device_id
|
|
261
|
+
- **永远不要显示 device_id**--这是内部标识符
|
|
247
262
|
- 只展示名字 + 三句话 + 你写的匹配理由
|
|
248
263
|
- 不要泄露对方的平台或用户名
|
|
249
264
|
- 联系方式只在用户明确同意时分享
|
|
250
265
|
- GPS 不原始存储
|
|
251
266
|
|
|
252
|
-
### Time Decay
|
|
253
|
-
- Event 后 0-7
|
|
254
|
-
- 7-30
|
|
255
|
-
- 30
|
|
267
|
+
### Time Decay - 可见性衰减
|
|
268
|
+
- Event 后 0-7 天:全部参与者互相可见
|
|
269
|
+
- 7-30 天:只有互相 scan 过 / 有共同活动的人可见
|
|
270
|
+
- 30 天后:需要新事件激活
|
|
256
271
|
|
|
257
|
-
### Heartbeat
|
|
258
|
-
Plugin 后台每 10 分钟查一次新匹配。看到 `[Antenna] 🎉`
|
|
272
|
+
### Heartbeat - 自动查匹配
|
|
273
|
+
Plugin 后台每 10 分钟查一次新匹配。看到 `[Antenna] 🎉` 时:
|
|
259
274
|
1. 调 `antenna_check_matches`
|
|
260
275
|
2. 告诉用户 + 展示对方名片
|
|
261
|
-
3.
|
|
276
|
+
3. 展示联系方式(如果有)
|
|
262
277
|
|
|
263
278
|
## Events
|
|
264
279
|
|
|
265
|
-
详见 EVENTS.md
|
|
280
|
+
详见 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`。
|
|
266
281
|
|
|
267
282
|
## Data Transparency
|
|
268
283
|
|
|
269
284
|
Antenna 只跟 Supabase (bcudjloikmpcqwcptuyd.supabase.co) 通信。
|
|
270
285
|
|
|
271
|
-
|
|
272
|
-
|
|
286
|
+
**发送的数据:** GPS(不原始存储)、名片文本、匹配状态、你选择分享的联系方式、Profile embedding。
|
|
287
|
+
**不发送的数据:** 你跟 agent 的对话、文件、浏览记录。
|
|
273
288
|
|
|
274
289
|
Source code: https://github.com/H1an1/Antenna
|