antenna-fyi 1.3.41 → 1.3.42
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/README.md +4 -1
- package/lib/cli.js +8 -6
- package/lib/core.js +15 -0
- package/lib/hermes-plugin/schemas.py +1 -0
- package/lib/hermes-plugin/tools.py +18 -0
- package/lib/mcp.js +4 -3
- package/lib/plugin-template/index.ts +22 -3
- package/package.json +1 -1
- package/skill/SKILL.md +7 -6
package/README.md
CHANGED
|
@@ -26,7 +26,10 @@ antenna checkin --id <platform>:<user_id> --lat 39.99 --lng 116.48
|
|
|
26
26
|
|
|
27
27
|
# View/edit your profile
|
|
28
28
|
antenna profile --id <platform>:<user_id>
|
|
29
|
-
antenna
|
|
29
|
+
antenna config --key <ant_xxx>
|
|
30
|
+
antenna profile --id <platform>:<user_id> --name Yi --line1 'Product Designer'
|
|
31
|
+
|
|
32
|
+
Profile writes require the user's Antenna API key from antenna.fyi/me. The CLI verifies the key and writes to the dashboard-linked `user:<uuid>` profile; agents should not create profiles from `channel:sender_id`.
|
|
30
33
|
|
|
31
34
|
# Accept a match
|
|
32
35
|
antenna accept --id <platform>:<user_id> --target <ref_or_id> --contact 'WeChat: yi'
|
package/lib/cli.js
CHANGED
|
@@ -68,10 +68,14 @@ export async function handleScan(f) {
|
|
|
68
68
|
|
|
69
69
|
export async function handleProfile(f) {
|
|
70
70
|
const id = resolveId(f);
|
|
71
|
-
if (!id) return console.error("Usage: antenna profile --id <platform>:<user_id> [--name Yi --personal-description '...' --looking-for '...' --conversation-style '...' --more-information '...' --tags 'AI,design,music' --city 'Beijing' --contact 'WeChat: yi' --slug mira --hide --visible true]");
|
|
71
|
+
if (!id) return console.error("Usage: antenna profile --id <platform>:<user_id> [--name Yi --personal-description '...' --looking-for '...' --conversation-style '...' --more-information '...' --tags 'AI,design,music' --city 'Beijing' --contact 'WeChat: yi' --slug mira --hide --visible true]\n Writes require: antenna config --key <your-api-key>");
|
|
72
72
|
if (f.name || f["personal-description"] !== undefined || f.line1 !== undefined || f["looking-for"] !== undefined || f.line2 !== undefined || f["conversation-style"] !== undefined || f.line3 !== undefined || f["more-information"] !== undefined || f.contact !== undefined || f.slug !== undefined || f.tags !== undefined || f.city !== undefined || f.visible !== undefined || f.hide !== undefined) {
|
|
73
|
+
const config = loadConfig();
|
|
74
|
+
if (!config.key) {
|
|
75
|
+
return console.error("❌ Profile writes require a dashboard API key. Run: antenna config --key <your-api-key>");
|
|
76
|
+
}
|
|
73
77
|
const visible = f.hide ? false : (f.visible !== undefined ? f.visible === 'true' || f.visible === true : undefined);
|
|
74
|
-
const payload = { device_id: id };
|
|
78
|
+
const payload = { device_id: config.device_id || id, api_key: config.key };
|
|
75
79
|
if (f.name) payload.display_name = f.name;
|
|
76
80
|
if (f["personal-description"] !== undefined) payload.line1 = f["personal-description"];
|
|
77
81
|
else if (f.line1 !== undefined) payload.line1 = f.line1;
|
|
@@ -85,8 +89,6 @@ export async function handleProfile(f) {
|
|
|
85
89
|
if (f.tags !== undefined) payload.interest_tags = typeof f.tags === 'string' ? f.tags.split(',').map(t => t.trim()).filter(Boolean) : [f.tags];
|
|
86
90
|
if (f.city !== undefined) payload.city = f.city;
|
|
87
91
|
if (visible !== undefined) payload.visible = visible;
|
|
88
|
-
const config = loadConfig();
|
|
89
|
-
if (config.key) payload.api_key = config.key;
|
|
90
92
|
const data = await setProfile(payload);
|
|
91
93
|
if (data?.error) {
|
|
92
94
|
console.error("❌ " + data.error);
|
|
@@ -445,7 +447,7 @@ export async function handleConfig(f) {
|
|
|
445
447
|
}
|
|
446
448
|
const config = loadConfig();
|
|
447
449
|
config.key = f.key;
|
|
448
|
-
config.device_id = result.device_id;
|
|
450
|
+
config.device_id = result.user_id ? `user:${result.user_id}` : result.device_id;
|
|
449
451
|
config.user_id = result.user_id;
|
|
450
452
|
config.display_name = result.display_name;
|
|
451
453
|
saveConfig(config);
|
|
@@ -1107,7 +1109,7 @@ Agent shortcuts:
|
|
|
1107
1109
|
Usage:
|
|
1108
1110
|
antenna scan --lat 39.99 --lng 116.48 [--radius 500] (max 1000) [--id <platform>:<user_id>]
|
|
1109
1111
|
antenna checkin --id <platform>:<user_id> --lat 39.99 --lng 116.48
|
|
1110
|
-
antenna profile --id <platform>:<user_id> [--name Yi --personal-description '...' --looking-for '...' --conversation-style '...' --more-information '...' --tags 'AI,design,music' --city 'Beijing' --contact 'WeChat: yi' --slug mira --hide --visible true]
|
|
1112
|
+
antenna profile --id <platform>:<user_id> [--name Yi --personal-description '...' --looking-for '...' --conversation-style '...' --more-information '...' --tags 'AI,design,music' --city 'Beijing' --contact 'WeChat: yi' --slug mira --hide --visible true] (writes require antenna config --key)
|
|
1111
1113
|
antenna accept --id <platform>:<user_id> --target <ref_or_device_id> [--contact 'WeChat: yi']
|
|
1112
1114
|
antenna pass --id <platform>:<user_id> --target <ref_or_device_id> (or --ref 1)
|
|
1113
1115
|
antenna matches --id <platform>:<user_id>
|
package/lib/core.js
CHANGED
|
@@ -301,6 +301,18 @@ export async function setProfile({
|
|
|
301
301
|
supabaseKey,
|
|
302
302
|
}) {
|
|
303
303
|
const sb = getClient(supabaseUrl, supabaseKey);
|
|
304
|
+
if (!api_key) {
|
|
305
|
+
throw new Error("Profile writes require the user's Antenna API key from antenna.fyi/me. Do not create an agent-only profile from sender_id/channel.");
|
|
306
|
+
}
|
|
307
|
+
const auth = await verifyApiKey({ key: api_key, supabaseUrl, supabaseKey });
|
|
308
|
+
if (!auth?.valid) {
|
|
309
|
+
throw new Error(auth?.error || "Invalid Antenna API key");
|
|
310
|
+
}
|
|
311
|
+
const dashboardDeviceId = auth.user_id ? `user:${auth.user_id}` : auth.device_id;
|
|
312
|
+
if (!dashboardDeviceId) {
|
|
313
|
+
throw new Error("API key verified but did not return a dashboard device_id");
|
|
314
|
+
}
|
|
315
|
+
device_id = dashboardDeviceId;
|
|
304
316
|
|
|
305
317
|
// Pack structured fields into matching_context JSON
|
|
306
318
|
let contextJson = matching_context;
|
|
@@ -411,6 +423,7 @@ export async function setProfile({
|
|
|
411
423
|
p_device_id: device_id,
|
|
412
424
|
p_matching_context: JSON.stringify(ctx),
|
|
413
425
|
p_visible: profile?.visible ?? true,
|
|
426
|
+
p_api_key: api_key,
|
|
414
427
|
});
|
|
415
428
|
} catch {}
|
|
416
429
|
}
|
|
@@ -432,6 +445,8 @@ export async function setProfile({
|
|
|
432
445
|
public_url: publicUrl,
|
|
433
446
|
gps_bind_url: bindUrl,
|
|
434
447
|
archetype: archetypeResult || null,
|
|
448
|
+
api_key_verified: true,
|
|
449
|
+
dashboard_device_id: device_id,
|
|
435
450
|
next_step: "Send the public_url and gps_bind_url to the user. The GPS link should be opened on their phone to share location.",
|
|
436
451
|
};
|
|
437
452
|
}
|
|
@@ -56,6 +56,7 @@ PROFILE_SCHEMA = {
|
|
|
56
56
|
"line3": {"type": "string", "description": "What you're looking for"},
|
|
57
57
|
"visible": {"type": "boolean", "description": "Visible to others"},
|
|
58
58
|
"matching_context": {"type": "string", "description": "Free-form context for AI matching (interests, goals, etc.)"},
|
|
59
|
+
"api_key": {"type": "string", "description": "Required for action='set': user's Antenna API key from antenna.fyi/me. Profile writes use the dashboard-linked user:<uuid> profile."},
|
|
59
60
|
},
|
|
60
61
|
"required": ["action", "sender_id", "channel", "chat_id"],
|
|
61
62
|
},
|
|
@@ -43,6 +43,19 @@ def _profile_slug_candidate(display_name: str | None, device_id: str) -> str:
|
|
|
43
43
|
return f"user-{digest}"
|
|
44
44
|
|
|
45
45
|
|
|
46
|
+
def _dashboard_device_id(sb, api_key: str | None) -> tuple[str | None, str | None]:
|
|
47
|
+
if not api_key:
|
|
48
|
+
return None, "Profile writes require the user's Antenna API key from antenna.fyi/me. Do not create an agent-only profile from sender_id/channel."
|
|
49
|
+
resp = sb.rpc("verify_api_key", {"p_key": api_key}).execute()
|
|
50
|
+
data = resp.data or {}
|
|
51
|
+
if not data.get("valid"):
|
|
52
|
+
return None, data.get("error") or "Invalid Antenna API key"
|
|
53
|
+
device_id = f"user:{data.get('user_id')}" if data.get("user_id") else data.get("device_id")
|
|
54
|
+
if not device_id:
|
|
55
|
+
return None, "API key verified but did not return a dashboard device_id"
|
|
56
|
+
return device_id, None
|
|
57
|
+
|
|
58
|
+
|
|
46
59
|
def _get_url():
|
|
47
60
|
return os.environ.get("ANTENNA_SUPABASE_URL") or os.environ.get("ANTENNA_URL") or BUILTIN_URL
|
|
48
61
|
|
|
@@ -191,6 +204,9 @@ def handle_profile(params: dict) -> str:
|
|
|
191
204
|
return _ok({"exists": True, "profile": resp.data})
|
|
192
205
|
|
|
193
206
|
# set
|
|
207
|
+
did, auth_error = _dashboard_device_id(sb, params.get("api_key"))
|
|
208
|
+
if auth_error:
|
|
209
|
+
return _ok({"error": auth_error})
|
|
194
210
|
rpc_params = {
|
|
195
211
|
"p_device_id": did,
|
|
196
212
|
"p_display_name": params.get("display_name"),
|
|
@@ -224,6 +240,8 @@ def handle_profile(params: dict) -> str:
|
|
|
224
240
|
"updated": True,
|
|
225
241
|
"profile": resp.data,
|
|
226
242
|
"public_url": public_url,
|
|
243
|
+
"api_key_verified": True,
|
|
244
|
+
"dashboard_device_id": did,
|
|
227
245
|
"next_step": "IMPORTANT: 1) Send public_url to the user — this is their shareable profile link. 2) Call antenna_bind to generate a GPS link. Do not skip either step.",
|
|
228
246
|
})
|
|
229
247
|
return _ok({"error": "upsert_profile failed"})
|
package/lib/mcp.js
CHANGED
|
@@ -128,8 +128,9 @@ export async function startMcpServer() {
|
|
|
128
128
|
links: z.array(z.string()).optional().describe("Social links shown on the card footer (up to 3)"),
|
|
129
129
|
is_active: z.boolean().optional().describe("Whether the profile is active or quiet"),
|
|
130
130
|
visible: z.boolean().optional().default(true),
|
|
131
|
+
api_key: z.string().optional().describe("Required for action='set': the user's Antenna API key from antenna.fyi/me. Profile writes use this key and the dashboard-linked user:<uuid> device_id."),
|
|
131
132
|
},
|
|
132
|
-
async ({ action, sender_id, channel, display_name, personal_description, looking_for, conversation_style, more_information, interest_tags, city, links, is_active, visible }) => {
|
|
133
|
+
async ({ action, sender_id, channel, display_name, personal_description, looking_for, conversation_style, more_information, interest_tags, city, links, is_active, visible, api_key }) => {
|
|
133
134
|
const deviceId = deriveDeviceId(sender_id, channel);
|
|
134
135
|
try {
|
|
135
136
|
if (action === "get") {
|
|
@@ -139,8 +140,8 @@ export async function startMcpServer() {
|
|
|
139
140
|
: { profile: null, message: "还没有名片。跟用户聊聊他们是谁、做什么、想认识什么人,然后帮他们创建。", fields: PROFILE_FIELDS };
|
|
140
141
|
return jsonResult(await withMatchNotifications(deviceId, result));
|
|
141
142
|
}
|
|
142
|
-
const data = await setProfile({ device_id: deviceId, display_name, line1: personal_description, line2: looking_for, line3: conversation_style, matching_context: more_information, interest_tags, city, links, is_active, visible });
|
|
143
|
-
return jsonResult(await withMatchNotifications(deviceId, { saved: true, profile: data }));
|
|
143
|
+
const data = await setProfile({ device_id: deviceId, display_name, line1: personal_description, line2: looking_for, line3: conversation_style, matching_context: more_information, interest_tags, city, links, is_active, visible, api_key });
|
|
144
|
+
return jsonResult(await withMatchNotifications(data.dashboard_device_id || deviceId, { saved: true, profile: data }));
|
|
144
145
|
} catch (e) {
|
|
145
146
|
return jsonResult({ error: e.message });
|
|
146
147
|
}
|
|
@@ -77,6 +77,18 @@ function profileSlugCandidate(displayName: string | null | undefined, deviceId:
|
|
|
77
77
|
return `user-${createHash("sha1").update(deviceId).digest("hex").slice(0, 10)}`;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
async function resolveDashboardDeviceId(supabase: SupabaseClient, apiKey: string | null | undefined) {
|
|
81
|
+
if (!apiKey) {
|
|
82
|
+
return { error: "Profile writes require the user's Antenna API key from antenna.fyi/me. Do not create an agent-only profile from sender_id/channel." };
|
|
83
|
+
}
|
|
84
|
+
const { data, error } = await supabase.rpc("verify_api_key", { p_key: apiKey });
|
|
85
|
+
if (error) return { error: error.message };
|
|
86
|
+
if (!data?.valid) return { error: data?.error || "Invalid Antenna API key" };
|
|
87
|
+
const deviceId = data.user_id ? `user:${data.user_id}` : data.device_id;
|
|
88
|
+
if (!deviceId) return { error: "API key verified but did not return a dashboard device_id" };
|
|
89
|
+
return { deviceId, userId: data.user_id, displayName: data.display_name };
|
|
90
|
+
}
|
|
91
|
+
|
|
80
92
|
function isRateLimited(deviceId: string): boolean {
|
|
81
93
|
const now = Date.now();
|
|
82
94
|
const last = _lastScanTime.get(deviceId);
|
|
@@ -411,13 +423,14 @@ export default function register(api: any) {
|
|
|
411
423
|
line2: { type: "string", description: "Second line (what you're into)" },
|
|
412
424
|
line3: { type: "string", description: "Third line (what you're looking for)" },
|
|
413
425
|
visible: { type: "boolean", description: "Whether to be visible to others" },
|
|
426
|
+
api_key: { type: "string", description: "Required for action='set': user's Antenna API key from antenna.fyi/me. Profile writes use the dashboard-linked user:<uuid> profile." },
|
|
414
427
|
},
|
|
415
428
|
required: ["action", "sender_id", "channel"],
|
|
416
429
|
},
|
|
417
430
|
async execute(_id: string, params: any) {
|
|
418
431
|
const cfg = getConfig(api);
|
|
419
432
|
const supabase = getSupabase(cfg);
|
|
420
|
-
|
|
433
|
+
let deviceId = deriveDeviceId(params.sender_id, params.channel);
|
|
421
434
|
|
|
422
435
|
if (params.action === "get") {
|
|
423
436
|
const { data, error } = await supabase.rpc("get_profile", { p_device_id: deviceId });
|
|
@@ -431,12 +444,16 @@ export default function register(api: any) {
|
|
|
431
444
|
});
|
|
432
445
|
}
|
|
433
446
|
|
|
447
|
+
const resolved = await resolveDashboardDeviceId(supabase, params.api_key);
|
|
448
|
+
if (resolved.error) return ok({ error: resolved.error });
|
|
449
|
+
deviceId = resolved.deviceId!;
|
|
450
|
+
|
|
434
451
|
const { data, error } = await supabase.rpc("upsert_profile", {
|
|
435
452
|
p_device_id: deviceId,
|
|
436
453
|
p_display_name: params.display_name ?? null, p_emoji: null,
|
|
437
454
|
p_line1: params.line1 ?? null, p_line2: params.line2 ?? null,
|
|
438
455
|
p_line3: params.line3 ?? null, p_visible: params.visible ?? true,
|
|
439
|
-
p_api_key:
|
|
456
|
+
p_api_key: params.api_key,
|
|
440
457
|
});
|
|
441
458
|
|
|
442
459
|
if (error) return ok({ error: error.message });
|
|
@@ -447,7 +464,7 @@ export default function register(api: any) {
|
|
|
447
464
|
let profileSlug = profile?.profile_slug || null;
|
|
448
465
|
if (!profileSlug) {
|
|
449
466
|
const targetSlug = profileSlugCandidate(params.display_name, deviceId);
|
|
450
|
-
const { data: slugResult } = await supabase.rpc("set_profile_slug", { p_device_id: deviceId, p_slug: targetSlug, p_api_key:
|
|
467
|
+
const { data: slugResult } = await supabase.rpc("set_profile_slug", { p_device_id: deviceId, p_slug: targetSlug, p_api_key: params.api_key });
|
|
451
468
|
if (slugResult?.set) profileSlug = targetSlug;
|
|
452
469
|
}
|
|
453
470
|
if (profileSlug) publicUrl = `https://www.antenna.fyi/p/${profileSlug}`;
|
|
@@ -458,6 +475,8 @@ export default function register(api: any) {
|
|
|
458
475
|
profile: { display_name: data.display_name,
|
|
459
476
|
line1: data.line1, line2: data.line2, line3: data.line3, visible: data.visible },
|
|
460
477
|
public_url: publicUrl,
|
|
478
|
+
api_key_verified: true,
|
|
479
|
+
dashboard_device_id: deviceId,
|
|
461
480
|
next_step: "IMPORTANT: Send the public_url to the user — this is their shareable profile link. Then call antenna_bind to generate a GPS link. Do not skip either step.",
|
|
462
481
|
});
|
|
463
482
|
},
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -38,9 +38,9 @@ Plugin 安装后,agent **主动**开始引导,不要等用户问。
|
|
|
38
38
|
**第一步:拿到 API key → 配置**
|
|
39
39
|
> "嘿,你装了 Antenna--它能帮你发现有意思的人。先确认一下,你在 antenna.fyi 注册过了吗?拿到 API key 了吗?"
|
|
40
40
|
|
|
41
|
-
用户给了 API key 后,调 `antenna config --key <key>` 验证。这会返回 `user_id` 和 `device_id`。
|
|
41
|
+
用户给了 API key 后,调 `antenna config --key <key>` 验证。这会返回 `user_id` 和 dashboard 绑定的 `device_id`。
|
|
42
42
|
|
|
43
|
-
**⚠️
|
|
43
|
+
**⚠️ Profile 写入必须通过用户的 Antenna API key。** 调 `antenna_profile(action="set")` 时传 `api_key`,tool 会验证 API key 并使用返回的 dashboard `device_id`(格式 `user:xxx`) 写入。不要自己拼 `channel:sender_id`,不要在用户拿到 API key 前凭空创建 profile。这样 agent 填的内容才会显示在 dashboard。
|
|
44
44
|
|
|
45
45
|
**第二步:聊天收集 → 生成名片 → 确认**
|
|
46
46
|
|
|
@@ -58,9 +58,9 @@ Plugin 安装后,agent **主动**开始引导,不要等用户问。
|
|
|
58
58
|
>
|
|
59
59
|
> 这样可以吗?要改哪里告诉我。
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
确认后调 `antenna_profile(action="set", api_key="ant_xxx")` 保存。**不要跳过确认。**
|
|
62
62
|
|
|
63
|
-
**⚠️
|
|
63
|
+
**⚠️ 保存 profile 时以 API key 为准;sender_id/channel 只用于上下文,不作为 profile 归属来源。**
|
|
64
64
|
|
|
65
65
|
**第三步:立刻推荐 2-3 个人**
|
|
66
66
|
|
|
@@ -96,12 +96,12 @@ openclaw cron add --every 1h --message "Check antenna matches: call antenna_chec
|
|
|
96
96
|
|
|
97
97
|
### Linking to antenna.fyi account
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
Legacy only: 如果用户之前通过旧版 agent 创建过 profile(没有网站账号),现在注册了 antenna.fyi:
|
|
100
100
|
1. 让用户从 antenna.fyi/me 复制 API key
|
|
101
101
|
2. 调 `antenna_link_account(api_key = "ant_xxx")`
|
|
102
102
|
3. 确认:"关联成功!你现在可以在 dashboard 上看到完整的 profile 和匹配记录了。"
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
这只用于迁移旧数据。新流程禁止先创建 agent-only profile 再绑定;必须先拿 API key,再通过 API 写入 dashboard profile。
|
|
105
105
|
|
|
106
106
|
## When to use
|
|
107
107
|
|
|
@@ -164,6 +164,7 @@ openclaw cron add --every 1h --message "Check antenna matches: call antenna_chec
|
|
|
164
164
|
- `action`:"get" 或 "set"
|
|
165
165
|
- `sender_id`, `channel`, `chat_id`
|
|
166
166
|
- "set" 时传:`display_name`, `personal_description`, `looking_for`, `conversation_style`, `visible`, `matching_context`
|
|
167
|
+
- "set" 必须传 `api_key`。tool 会验证 key,并写入 API key 绑定的 dashboard profile;不要用 `sender_id/channel` 创建独立 profile。
|
|
167
168
|
|
|
168
169
|
名片内容:
|
|
169
170
|
- **display_name**:显示名称
|