antenna-openclaw-plugin 0.1.1 → 0.2.0

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/index.ts CHANGED
@@ -88,6 +88,7 @@ function fuzzyCoords(lat: number, lng: number) {
88
88
  }
89
89
 
90
90
  // TODO: Replace with LLM-based matching for better Chinese support
91
+ // Kept for potential future server-side pre-filtering
91
92
  function extractWords(profile: Partial<Profile>): string[] {
92
93
  const text = [profile.line1, profile.line2, profile.line3]
93
94
  .filter(Boolean)
@@ -116,7 +117,7 @@ export default function register(api: any) {
116
117
  api.registerTool({
117
118
  name: "antenna_scan",
118
119
  description:
119
- "Scan for nearby people at a given location. Returns matched profiles with reasons. Use when the user shares their location or asks 'who is nearby'.",
120
+ "Scan for nearby people at a given location. Returns raw profile cards of nearby people — the agent should read these cards and decide who to recommend based on its understanding of the user. Use when the user shares their location or asks 'who is nearby'.",
120
121
  parameters: {
121
122
  type: "object",
122
123
  properties: {
@@ -133,10 +134,9 @@ export default function register(api: any) {
133
134
  const supabase = getSupabase(cfg);
134
135
  const deviceId = deriveDeviceId(params.sender_id, params.channel);
135
136
  const radius = params.radius_m ?? cfg.defaultRadiusM ?? 500;
136
- const maxMatches = cfg.maxMatches ?? 5;
137
137
 
138
138
  if (isRateLimited(deviceId)) {
139
- return ok({ matches: [], message: "刚刚才扫描过,稍等一会儿再试。", rate_limited: true });
139
+ return ok({ nearby: [], message: "刚刚才扫描过,稍等一会儿再试。", rate_limited: true });
140
140
  }
141
141
 
142
142
  const fuzzy = fuzzyCoords(params.lat, params.lng);
@@ -157,41 +157,22 @@ export default function register(api: any) {
157
157
  const others = (nearby ?? []).filter((p: Profile) => p.device_id !== deviceId);
158
158
 
159
159
  if (others.length === 0) {
160
- return ok({ matches: [], message: `在 ${radius}m 范围内没有发现其他人。试试扩大范围?` });
161
- }
162
-
163
- const { data: myProfile } = await supabase.rpc("get_profile", { p_device_id: deviceId });
164
-
165
- const myWords = myProfile ? extractWords(myProfile) : [];
166
- const scored: MatchResult[] = others.map((p: Profile) => {
167
- const theirWords = extractWords(p);
168
- const overlap = myWords.filter((w: string) => theirWords.includes(w));
169
- const score = myWords.length > 0 ? Math.min(overlap.length / myWords.length, 1) : 0;
170
- const reason = overlap.length > 0
171
- ? `你们都提到了 ${overlap.slice(0, 3).join("、")}——可能聊得来`
172
- : `${p.display_name || p.emoji || "TA"} 就在附近`;
173
- return { device_id: p.device_id, display_name: p.display_name, emoji: p.emoji,
174
- line1: p.line1, line2: p.line2, line3: p.line3, score, reason };
175
- });
176
-
177
- scored.sort((a, b) => b.score - a.score);
178
- const topMatches = scored.slice(0, maxMatches);
179
-
180
- const expiryHours = cfg.matchExpiryHours ?? 24;
181
- for (const m of topMatches) {
182
- await supabase.rpc("upsert_match", {
183
- p_device_id_a: deviceId, p_device_id_b: m.device_id,
184
- p_reason: m.reason, p_score: m.score, p_status: "pending", p_expires_hours: expiryHours,
185
- });
160
+ return ok({ nearby: [], message: `在 ${radius}m 范围内没有发现其他人。试试扩大范围?` });
186
161
  }
187
162
 
163
+ // Return raw profile cards — the agent decides who to recommend
188
164
  return ok({
189
- matches: topMatches.map((m) => ({
190
- emoji: m.emoji || "👤", name: m.display_name || "匿名",
191
- line1: m.line1, line2: m.line2, line3: m.line3,
192
- score: m.score, reason: m.reason,
165
+ nearby: others.map((p: Profile) => ({
166
+ device_id: p.device_id,
167
+ emoji: p.emoji || "👤",
168
+ name: p.display_name || "匿名",
169
+ line1: p.line1,
170
+ line2: p.line2,
171
+ line3: p.line3,
193
172
  })),
194
- total_nearby: others.length, radius_m: radius,
173
+ total: others.length,
174
+ radius_m: radius,
175
+ instruction: "根据你对用户的了解(记忆、偏好、最近的状态),判断哪些人值得推荐,为每个推荐写一句个性化的匹配理由。",
195
176
  });
196
177
  },
197
178
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antenna-openclaw-plugin",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Antenna — agent-mediated nearby people discovery for OpenClaw",
5
5
  "openclaw": {
6
6
  "extensions": ["./index.ts"]
@@ -19,12 +19,19 @@ You have access to the Antenna plugin tools for location-based social discovery.
19
19
  ## Tools
20
20
 
21
21
  ### `antenna_scan`
22
- Scan for nearby people. Use when you receive a location.
22
+ Scan for nearby people. Returns **raw profile cards** no scores, no pre-matching. **You are the matching engine.**
23
23
  - `lat`, `lng`: coordinates (from `LocationLat`/`LocationLon` context, or geocoded from user input)
24
24
  - `radius_m`: search radius (default 500m)
25
25
  - `sender_id`: the user's id from message context
26
26
  - `channel`: the channel name (telegram, whatsapp, discord, etc.)
27
27
 
28
+ After receiving the nearby profiles, **you decide** who to recommend:
29
+ - Use everything you know about the user: their SOUL.md, memory, recent conversations, interests, current mood
30
+ - Compare each nearby person's three-line card against your understanding of the user
31
+ - Write a personalized match reason for each person you recommend
32
+ - Skip people who clearly aren't a match — don't recommend everyone
33
+ - If you're unsure, lean toward recommending (let the user decide)
34
+
28
35
  ### `antenna_profile`
29
36
  View or update the user's name card.
30
37
  - `action`: "get" or "set"
@@ -50,29 +57,67 @@ Check for mutual matches and contact info updates.
50
57
 
51
58
  ## Behavior guidelines
52
59
 
53
- ### First-time user
54
- If the user doesn't have a profile yet, guide them to create one BEFORE scanning:
55
- 1. Ask for a name, an emoji, and three short lines about themselves
56
- 2. Use `antenna_profile` action="set" to save it
57
- 3. Then proceed to scan
58
-
59
- ### Showing results
60
- Present matches conversationally, not as a data dump:
61
- - Lead with the emoji and name
62
- - Show their three lines
63
- - Include the match reason naturally
64
- - Ask if they want to accept any match
65
-
66
- Example:
67
- > 📡 附近发现 3 个人:
68
- >
69
- > 🎸 **小林** — 吉他手,喜欢后摇和 shoegaze,在找人一起 jam
70
- > → 你们都提到了音乐和后摇——可能聊得来
60
+ ### First-time user — 聊天式引导(不要让用户填表)
61
+
62
+ **绝对不要**一次性说"请填写 emoji、名字、三句话介绍"——这会让用户懵掉。
63
+
64
+ 用聊天的方式一步一步引导:
65
+
66
+ **第一步:开场**
67
+ > "嘿,第一次用 Antenna?我帮你做张名片,附近的人会看到它。先聊几句就行。"
68
+
69
+ **第二步:问职业/身份**(→ line1)
70
+ > "你平时做什么?工作、学生、自由职业、还是别的?"
71
+
72
+ 用户可能回答很长,也可能很短。不管怎样,你提炼成一句简短的话。
73
+
74
+ **第三步:问兴趣**(→ line2)
75
+ > "最近在玩什么?或者对什么特别感兴趣?"
76
+
77
+ **第四步:问意图**(→ line3)
78
+ > "来这儿想认识什么样的人?或者找什么?"
79
+
80
+ **第五步:问名字和 emoji**
81
+ > "最后——你想被叫什么?再选个 emoji 代表你自己。"
82
+
83
+ **第六步:确认**
84
+ 把名片组装好,展示给用户确认:
85
+ > 你的名片:
71
86
  >
72
- > 🏃 **Alex** — 跑步爱好者,每周三晚朝阳公园
73
- > → 就在附近
87
+ > 🎸 **小林**
88
+ > 吉他手,在乐队弹后摇
89
+ > 喜欢 shoegaze 和 post-rock,最近在听 Mogwai
90
+ > 找人一起 jam 或者聊音乐
74
91
  >
75
- > 想跟谁打个招呼?
92
+ > 看看有没有要改的?OK 的话我就存了。
93
+
94
+ 用户说 OK → `antenna_profile` action="set" 保存。
95
+ 用户说要改 → 改完再确认。
96
+
97
+ **关键原则:**
98
+ - 每次只问一个问题
99
+ - 用户说的原话尽量保留,不要过度润色
100
+ - 可以帮用户缩短太长的回答,但要让用户确认
101
+ - 如果用户不想回答某一项,留空也行("那这行先空着,以后想加再说")
102
+ - 整个过程应该像跟朋友聊天,不像填表
103
+
104
+ ### Showing results — 你来判断,不是服务器
105
+
106
+ `antenna_scan` 返回的是附近所有人的名片,**没有打分、没有预匹配**。你需要:
107
+
108
+ 1. 读每个人的名片(emoji、name、line1/2/3)
109
+ 2. 结合你对用户的全部了解,判断谁值得推荐
110
+ 3. 为每个推荐的人写一句**个性化的理由**——不是"你们都提到了 X",而是真正有洞察的话
111
+
112
+ 比如你知道用户最近在学吉他,看到附近有人写"组乐队找吉他手":
113
+ > 🎸 **小林** — 在组后摇乐队,找吉他手
114
+ > → 你不是最近在学吉他吗?这人在找吉他手诶
115
+
116
+ 比如你知道用户是设计师,对方也做设计:
117
+ > 🎨 **Kira** — UI 设计师,在做 AI 产品
118
+ > → 你们都做 AI 方向的设计,可以聊聊各自的方法论
119
+
120
+ **不要推荐所有人。** 如果附近 5 个人里只有 1 个真的匹配,就只推 1 个。质量 > 数量。
76
121
 
77
122
  ### Accepting & contact exchange
78
123
  When the user wants to accept a match: