antenna-fyi 1.3.25 → 1.3.27

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/core.js CHANGED
@@ -45,6 +45,22 @@ async function generateMatchReason(myLines, theirLines) {
45
45
  } catch { return null; }
46
46
  }
47
47
 
48
+ async function generateArchetypeReason(archetype, profileText) {
49
+ try {
50
+ const res = await fetch(`${_url || DEFAULT_URL}/functions/v1/generate-archetype`, {
51
+ method: "POST",
52
+ headers: {
53
+ "Content-Type": "application/json",
54
+ "Authorization": `Bearer ${process.env.ANTENNA_SUPABASE_KEY || process.env.ANTENNA_KEY || DEFAULT_KEY}`,
55
+ },
56
+ body: JSON.stringify({ archetype, profile_text: profileText }),
57
+ });
58
+ if (!res.ok) return null;
59
+ const data = await res.json();
60
+ return data || null;
61
+ } catch { return null; }
62
+ }
63
+
48
64
  export function getClient(url, key) {
49
65
  const u = url || process.env.ANTENNA_SUPABASE_URL || process.env.ANTENNA_URL || DEFAULT_URL;
50
66
  const k = key || process.env.ANTENNA_SUPABASE_KEY || process.env.ANTENNA_KEY || DEFAULT_KEY;
@@ -311,6 +327,7 @@ export async function setProfile({
311
327
  // Read back profile to get slug for public page link
312
328
  let publicUrl = null;
313
329
  let bindUrl = null;
330
+ let archetypeResult = null;
314
331
  try {
315
332
  const profile = await getProfile({ device_id, supabaseUrl, supabaseKey });
316
333
  if (profile?.profile_slug) {
@@ -318,6 +335,50 @@ export async function setProfile({
318
335
  }
319
336
  } catch {}
320
337
 
338
+ // Generate personalized archetype description via LLM
339
+ try {
340
+ const profileText = [line1, line2, line3, matching_context].filter(Boolean).join(". ");
341
+ if (profileText) {
342
+ // Simple keyword-based archetype detection (same logic as frontend)
343
+ const corpus = profileText.toLowerCase();
344
+ const archetypeKeywords = {
345
+ Prometheus: ["ai", "agent", "llm", "founder", "startup", "build", "developer", "hacker", "tools", "\u667a\u80fd\u4f53", "\u521b\u4e1a", "\u5f00\u53d1"],
346
+ Athena: ["product", "strategy", "research", "design", "craft", "pm", "ux", "\u4ea7\u54c1", "\u8bbe\u8ba1", "\u7814\u7a76"],
347
+ Hermes: ["network", "connect", "community", "social", "bridge", "\u793e\u4ea4", "\u8fde\u63a5", "\u793e\u533a"],
348
+ Apollo: ["music", "media", "content", "creator", "writing", "taste", "\u97f3\u4e50", "\u5185\u5bb9", "\u521b\u4f5c"],
349
+ Artemis: ["independent", "explore", "freelance", "health", "outdoor", "\u72ec\u7acb", "\u63a2\u7d22"],
350
+ Aphrodite: ["beauty", "brand", "fashion", "relationship", "\u7f8e", "\u54c1\u724c", "\u65f6\u5c1a"],
351
+ Dionysus: ["event", "culture", "party", "art", "festival", "\u6d3b\u52a8", "\u6587\u5316", "\u827a\u672f"],
352
+ Hades: ["finance", "invest", "infrastructure", "backend", "security", "\u6295\u8d44", "\u91d1\u878d", "\u67b6\u6784"],
353
+ Persephone: ["transform", "cross", "research", "academic", "bridge", "\u8de8\u754c", "\u7814\u7a76", "\u5b66\u672f"],
354
+ Odysseus: ["founder", "journey", "resilience", "travel", "startup", "\u521b\u4e1a", "\u65c5\u884c"],
355
+ };
356
+ let bestArchetype = "Prometheus";
357
+ let bestScore = 0;
358
+ for (const [role, keywords] of Object.entries(archetypeKeywords)) {
359
+ const score = keywords.reduce((s, kw) => s + (corpus.includes(kw) ? 1 : 0), 0);
360
+ if (score > bestScore) { bestScore = score; bestArchetype = role; }
361
+ }
362
+ archetypeResult = await generateArchetypeReason(bestArchetype, profileText);
363
+ if (archetypeResult?.reason) {
364
+ archetypeResult.archetype = bestArchetype;
365
+ // Write archetype back to matching_context so dashboard can display it
366
+ try {
367
+ const profile = await getProfile({ device_id, supabaseUrl, supabaseKey });
368
+ let ctx = {};
369
+ try { ctx = JSON.parse(profile?.matching_context || "{}"); } catch {}
370
+ ctx.archetypeOverride = { name: bestArchetype, reason: archetypeResult.reason, reasonZh: archetypeResult.reasonZh };
371
+ await sb.rpc("upsert_profile", {
372
+ p_device_id: device_id,
373
+ p_matching_context: JSON.stringify(ctx),
374
+ });
375
+ } catch {}
376
+ }
377
+ }
378
+ } catch (e) {
379
+ console.error("Archetype generation failed (non-fatal):", e.message);
380
+ }
381
+
321
382
  // Auto-generate GPS bind link
322
383
  try {
323
384
  const bind = await createBindToken({ device_id, supabaseUrl, supabaseKey });
@@ -328,6 +389,7 @@ export async function setProfile({
328
389
  ...data,
329
390
  public_url: publicUrl,
330
391
  gps_bind_url: bindUrl,
392
+ archetype: archetypeResult || null,
331
393
  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.",
332
394
  };
333
395
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antenna-fyi",
3
- "version": "1.3.25",
3
+ "version": "1.3.27",
4
4
  "description": "Antenna \u2014 nearby people discovery. CLI + MCP server + OpenClaw skill & plugin, all in one package.",
5
5
  "type": "module",
6
6
  "bin": {
package/skill/SKILL.md CHANGED
@@ -24,50 +24,56 @@ Antenna 帮你的用户发现值得认识的人。不只是"附近的人"--profi
24
24
  - 如果没有,用 CLI:`antenna <command> [flags]`。
25
25
  - **所有 tool 必须传 `chat_id`**--Antenna 用它发通知。
26
26
 
27
- ## Onboarding 装上后主动引导
27
+ ## Onboarding - 装上后主动引导
28
28
 
29
- Plugin 安装后,agent **主动**开始引导,不要等用户问。
29
+ Plugin 安装后,agent **主动**开始引导,不要等用户问。
30
30
 
31
- **前置条件:** 用户需要先在 antenna.fyi 注册账号,从 Dashboard 获取 API key。如果没注册,引导去 antenna.fyi。
31
+ **前置条件:** 用户需要先在 antenna.fyi 注册账号,从 Dashboard 获取 API key。如果没注册,引导去 antenna.fyi。
32
32
 
33
33
  ### 流程
34
34
 
35
- **第一步:拿到 API key → 配置**
36
- > "嘿,你装了 Antenna——它能帮你发现有意思的人。先确认一下,你在 antenna.fyi 注册过了吗?拿到 API key 了吗?"
35
+ **第一步:拿到 API key → 配置**
36
+ > "嘿,你装了 Antenna--它能帮你发现有意思的人。先确认一下,你在 antenna.fyi 注册过了吗?拿到 API key 了吗?"
37
37
 
38
- 用户给了 API key 后,调 `antenna config --key <key>` 验证。这会返回 `user_id` 和 `device_id`。
38
+ 用户给了 API key 后,调 `antenna config --key <key>` 验证。这会返回 `user_id` 和 `device_id`。
39
39
 
40
- **⚠️ 之后所有操作必须用 API key 验证返回的 device_id(格式 `user:xxx`)。不要自己拼 `channel:sender_id`。** 这样 agent 创建的 profile 才能在 dashboard 上显示。
40
+ **⚠️ 之后所有操作必须用 API key 验证返回的 device_id(格式 `user:xxx`)。不要自己拼 `channel:sender_id`。** 这样 agent 创建的 profile 才能在 dashboard 上显示。
41
41
 
42
- **第二步:聊天收集 → 生成名片 → 确认**
42
+ **第二步:聊天收集 → 生成名片 → 确认**
43
43
 
44
- 跟用户聊几句,了解他们是谁、做什么、想认识什么人。然后 agent 自己生成:
45
- - more_information(~200 字,给 agent 匹配用的私密上下文,不展示给别人)
44
+ 跟用户聊几句,了解他们是谁、做什么、想认识什么人。然后 agent 自己生成:
45
+ - more_information(~200 字,给 agent 匹配用的私密上下文,不展示给别人)
46
46
  - 从中提炼 line1/2/3 + display_name
47
47
 
48
- 展示预览给用户确认:
49
- > 你的名片:
48
+ 展示预览给用户确认:
49
+ > 你的名片:
50
50
  > Yi
51
- > · Product Designer,做 AI 搜索体验
51
+ > · Product Designer,做 AI 搜索体验
52
52
  > · 对 AI agent、音乐、游泳感兴趣
53
53
  > · 想认识做 AI 产品和独立创作的人
54
54
  >
55
- > 这样可以吗?要改哪里告诉我。
55
+ > 这样可以吗?要改哪里告诉我。
56
56
 
57
57
  确认后用 config 里的 device_id 调 `antenna_profile(action="set")` 保存。**不要跳过确认。**
58
58
 
59
- **⚠️ sender_id 用 config 里的 device_id,不要用 channel:sender_id。**
59
+ **⚠️ sender_id 用 config 里的 device_id,不要用 channel:sender_id。**
60
60
 
61
61
  **第三步:立刻推荐 2-3 个人**
62
62
 
63
63
  名片存好后,**立刻**调 `antenna_initial_recommendations` 获取首次推荐:
64
64
  > "名片存好了!我先帮你看看有谁跟你特别像--"
65
65
 
66
- 这是一次性的首次推荐(不消耗每日 discover 额度)。如果返回空,说明还没有足够的用户--跳过这步,继续 antenna_bind。
66
+ 这是一次性的首次推荐(不消耗每日 discover 额度)。如果返回空,说明还没有足够的用户——跳过这步。
67
67
 
68
- **第四步:调 antenna_bind 获取 GPS 链接**
68
+ **第四步:发公开链接给用户**
69
69
 
70
- **强制。** 名片存好后**必须立刻**调用 `antenna_bind` 生成链接并发给用户:
70
+ Profile 存好后,返回里会有 `public_url`(如 `antenna.fyi/p/yi`)。**必须发给用户:**
71
+ > "这是你的公开名片链接:[public_url]
72
+ > 你可以把它发给别人,对方的 agent 看到链接就能直接 accept 你。"
73
+
74
+ **第五步:调 antenna_bind 获取 GPS 链接**
75
+
76
+ **强制。** 名片存好后**必须立刻**调用 `antenna_bind` 生成链接并发给用户:
71
77
  > "点这个链接,在手机浏览器打开,允许定位就行:[链接]"
72
78
 
73
79
  GPS 不是 Antenna 的唯一入口,但它开启了附近发现的能力。