antenna-openclaw-plugin 1.3.40 → 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 +2 -0
- package/index.ts +41 -4
- package/package.json +1 -1
- package/skills/antenna/SKILL.md +9 -2
package/README.md
CHANGED
|
@@ -83,6 +83,8 @@ Supabase
|
|
|
83
83
|
└── pg_cron cleanup — hourly expired match cleanup
|
|
84
84
|
```
|
|
85
85
|
|
|
86
|
+
Profile writes require the user's Antenna API key from antenna.fyi/me. Agents must pass `api_key` to `antenna_profile(action="set")`; the plugin verifies it and writes to the dashboard-linked `user:<uuid>` profile instead of creating a profile from `channel:sender_id`.
|
|
87
|
+
|
|
86
88
|
## Supported platforms
|
|
87
89
|
|
|
88
90
|
Location auto-detection (OpenClaw parses the coordinates):
|
package/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createClient, SupabaseClient } from "@supabase/supabase-js";
|
|
2
2
|
import { execSync } from "child_process";
|
|
3
|
+
import { createHash } from "crypto";
|
|
3
4
|
|
|
4
5
|
// ─── Built-in Supabase config (shared backend, zero config) ─────────
|
|
5
6
|
|
|
@@ -72,6 +73,28 @@ function getSupabase(cfg: AntennaConfig): SupabaseClient {
|
|
|
72
73
|
return _supabaseClient;
|
|
73
74
|
}
|
|
74
75
|
|
|
76
|
+
function profileSlugCandidate(displayName: string | null | undefined, deviceId: string) {
|
|
77
|
+
const fromName = String(displayName || "")
|
|
78
|
+
.toLowerCase()
|
|
79
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
80
|
+
.replace(/^-|-$/g, "")
|
|
81
|
+
.substring(0, 30);
|
|
82
|
+
if (fromName) return fromName;
|
|
83
|
+
return `user-${createHash("sha1").update(deviceId).digest("hex").slice(0, 10)}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function resolveDashboardDeviceId(supabase: SupabaseClient, apiKey: string | null | undefined) {
|
|
87
|
+
if (!apiKey) {
|
|
88
|
+
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." };
|
|
89
|
+
}
|
|
90
|
+
const { data, error } = await supabase.rpc("verify_api_key", { p_key: apiKey });
|
|
91
|
+
if (error) return { error: error.message };
|
|
92
|
+
if (!data?.valid) return { error: data?.error || "Invalid Antenna API key" };
|
|
93
|
+
const deviceId = data.user_id ? `user:${data.user_id}` : data.device_id;
|
|
94
|
+
if (!deviceId) return { error: "API key verified but did not return a dashboard device_id" };
|
|
95
|
+
return { deviceId, userId: data.user_id, displayName: data.display_name };
|
|
96
|
+
}
|
|
97
|
+
|
|
75
98
|
function isRateLimited(deviceId: string): boolean {
|
|
76
99
|
const now = Date.now();
|
|
77
100
|
const last = _lastScanTime.get(deviceId);
|
|
@@ -420,13 +443,14 @@ export default function register(api: any) {
|
|
|
420
443
|
line3: { type: "string", description: "Third line (what you're looking for)" },
|
|
421
444
|
visible: { type: "boolean", description: "Whether to be visible to others" },
|
|
422
445
|
matching_context: { type: "string", description: "More information / free-form context for AI matching (interests, goals, background, etc.)" },
|
|
446
|
+
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." },
|
|
423
447
|
},
|
|
424
448
|
required: ["action", "sender_id", "channel", "chat_id"],
|
|
425
449
|
},
|
|
426
450
|
async execute(_id: string, params: any) {
|
|
427
451
|
const cfg = getConfig(api);
|
|
428
452
|
const supabase = getSupabase(cfg);
|
|
429
|
-
|
|
453
|
+
let deviceId = deriveDeviceId(params.sender_id, params.channel, params.chat_id);
|
|
430
454
|
|
|
431
455
|
if (params.action === "get") {
|
|
432
456
|
const { data, error } = await supabase.rpc("get_profile", { p_device_id: deviceId });
|
|
@@ -440,12 +464,17 @@ export default function register(api: any) {
|
|
|
440
464
|
});
|
|
441
465
|
}
|
|
442
466
|
|
|
467
|
+
const resolved = await resolveDashboardDeviceId(supabase, params.api_key);
|
|
468
|
+
if (resolved.error) return ok({ error: resolved.error });
|
|
469
|
+
deviceId = resolved.deviceId!;
|
|
470
|
+
|
|
443
471
|
const { data, error } = await supabase.rpc("upsert_profile", {
|
|
444
472
|
p_device_id: deviceId,
|
|
445
473
|
p_display_name: params.display_name ?? null, p_emoji: params.emoji ?? null,
|
|
446
474
|
p_line1: params.line1 ?? null, p_line2: params.line2 ?? null,
|
|
447
475
|
p_line3: params.line3 ?? null, p_visible: params.visible ?? true,
|
|
448
476
|
...(params.matching_context != null ? { p_matching_context: params.matching_context } : {}),
|
|
477
|
+
p_api_key: params.api_key,
|
|
449
478
|
});
|
|
450
479
|
|
|
451
480
|
if (error) return ok({ error: error.message });
|
|
@@ -455,8 +484,14 @@ export default function register(api: any) {
|
|
|
455
484
|
let archetypeResult = null;
|
|
456
485
|
try {
|
|
457
486
|
const { data: profile } = await supabase.rpc("get_profile", { p_device_id: deviceId });
|
|
458
|
-
|
|
459
|
-
|
|
487
|
+
let profileSlug = profile?.profile_slug || null;
|
|
488
|
+
if (!profileSlug) {
|
|
489
|
+
const targetSlug = profileSlugCandidate(params.display_name, deviceId);
|
|
490
|
+
const { data: slugResult } = await supabase.rpc("set_profile_slug", { p_device_id: deviceId, p_slug: targetSlug, p_api_key: params.api_key });
|
|
491
|
+
if (slugResult?.set) profileSlug = targetSlug;
|
|
492
|
+
}
|
|
493
|
+
if (profileSlug) {
|
|
494
|
+
publicUrl = `https://www.antenna.fyi/p/${profileSlug}`;
|
|
460
495
|
}
|
|
461
496
|
} catch {}
|
|
462
497
|
|
|
@@ -502,8 +537,10 @@ export default function register(api: any) {
|
|
|
502
537
|
profile: { display_name: data.display_name,
|
|
503
538
|
line1: data.line1, line2: data.line2, line3: data.line3, visible: data.visible },
|
|
504
539
|
public_url: publicUrl,
|
|
540
|
+
api_key_verified: true,
|
|
541
|
+
dashboard_device_id: deviceId,
|
|
505
542
|
archetype: archetypeResult || null,
|
|
506
|
-
next_step: "IMPORTANT: 1) Send the public_url to the user — this is their shareable profile link. 2) Tell the user their archetype and the personalized reason. 3) Call antenna_bind to generate a GPS link. Do not skip any step.",
|
|
543
|
+
next_step: "IMPORTANT: 1) Send the public_url to the user — this is their shareable profile link. If public_url is null, say profile link generation failed and retry profile save. 2) Tell the user their archetype and the personalized reason. 3) Call antenna_bind to generate a GPS link. Do not skip any step.",
|
|
507
544
|
});
|
|
508
545
|
},
|
|
509
546
|
});
|
package/package.json
CHANGED
package/skills/antenna/SKILL.md
CHANGED
|
@@ -25,6 +25,8 @@ Plugin 安装后,agent 应该**主动**开始引导,不要等用户问"怎么用
|
|
|
25
25
|
**第一步:打招呼 + 解释**
|
|
26
26
|
> "嘿,你装了 Antenna--它能帮你发现附近有意思的人。先确认一下,你在 antenna.fyi 注册过了吗?拿到 API key 了吗?有了的话我帮你做张名片,然后看看附近有谁。"
|
|
27
27
|
|
|
28
|
+
**硬约束:** Profile 写入必须通过用户从 antenna.fyi/me 拿到的 API key。`antenna_profile(action="set")` 必须传 `api_key`,tool 会验证 key 并写入 dashboard 绑定的 `user:<uuid>` profile。不要自己拼 `channel:sender_id`,不要在用户拿到 API key 前凭空创建 profile。
|
|
29
|
+
|
|
28
30
|
**第二步:聊天收集 → 生成名片 → 确认**
|
|
29
31
|
|
|
30
32
|
Agent 跟用户聊几句,了解他们是谁、做什么、对什么感兴趣、想认识什么人。然后 agent 自己完成以下工作(不需要用户参与):
|
|
@@ -40,7 +42,7 @@ Agent 跟用户聊几句,了解他们是谁、做什么、对什么感兴趣、
|
|
|
40
42
|
>
|
|
41
43
|
> 这样可以吗?要改哪里告诉我。
|
|
42
44
|
|
|
43
|
-
用户确认后才调 `antenna_profile(action="set")` 保存(matching_context + line1/2/3 + emoji + name 一起存)。
|
|
45
|
+
用户确认后才调 `antenna_profile(action="set", api_key="ant_xxx")` 保存(matching_context + line1/2/3 + emoji + name 一起存)。
|
|
44
46
|
用户要改 → 改完重新预览 → 再确认。
|
|
45
47
|
|
|
46
48
|
**不要跳过确认。名片是展示给别人看的,必须让用户看过才存。**
|
|
@@ -159,7 +161,8 @@ After receiving the nearby profiles, **you decide** who to recommend:
|
|
|
159
161
|
View or update the user's name card.
|
|
160
162
|
- `action`: "get" or "set"
|
|
161
163
|
- `sender_id`, `channel`: from context
|
|
162
|
-
- For "set": `display_name`, `emoji`, `line1`, `line2`, `line3`, `visible`, `matching_context`
|
|
164
|
+
- For "set": `api_key`, `display_name`, `emoji`, `line1`, `line2`, `line3`, `visible`, `matching_context`
|
|
165
|
+
- `api_key` is mandatory for writes. It must be the user's Antenna API key from antenna.fyi/me. The tool writes to the dashboard-linked `user:<uuid>` profile; do not create profiles from `sender_id/channel`.
|
|
163
166
|
|
|
164
167
|
The name card has:
|
|
165
168
|
- **emoji**: a single emoji that represents them
|
|
@@ -172,6 +175,10 @@ The name card has:
|
|
|
172
175
|
**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:
|
|
173
176
|
> "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."
|
|
174
177
|
|
|
178
|
+
After setting a profile, the tool returns `public_url`. **You must immediately send that link to the user** as their shareable public profile. If `public_url` is empty, retry profile save or report that link generation failed.
|
|
179
|
+
|
|
180
|
+
**i18n:** Save and show user-written profile content in the user's original language. Do not machine-translate their personal description, looking-for text, or conversation style. Antenna UI labels can switch language; the user's own text stays as written.
|
|
181
|
+
|
|
175
182
|
### `antenna_accept`
|
|
176
183
|
Accept a match after the user sees results. Can optionally include contact info to share.
|
|
177
184
|
- `sender_id`, `channel`, `target_device_id`
|