antenna-openclaw-plugin 0.1.2 → 0.2.1

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.2",
3
+ "version": "0.2.1",
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"
@@ -94,23 +101,23 @@ Check for mutual matches and contact info updates.
94
101
  - 如果用户不想回答某一项,留空也行("那这行先空着,以后想加再说")
95
102
  - 整个过程应该像跟朋友聊天,不像填表
96
103
 
97
- ### Showing results
98
- Present matches conversationally, not as a data dump:
99
- - Lead with the emoji and name
100
- - Show their three lines
101
- - Include the match reason naturally
102
- - Ask if they want to accept any match
104
+ ### Showing results — 你来判断,不是服务器
103
105
 
104
- Example:
105
- > 📡 附近发现 3 个人:
106
- >
107
- > 🎸 **小林** — 吉他手,喜欢后摇和 shoegaze,在找人一起 jam
108
- > 你们都提到了音乐和后摇——可能聊得来
109
- >
110
- > 🏃 **Alex** — 跑步爱好者,每周三晚朝阳公园
111
- > 就在附近
112
- >
113
- > 想跟谁打个招呼?
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 个。质量 > 数量。
114
121
 
115
122
  ### Accepting & contact exchange
116
123
  When the user wants to accept a match:
@@ -143,3 +150,13 @@ Everything is ephemeral:
143
150
  - Contact info shared through matches expires with the match
144
151
  - If neither side acts, the match disappears
145
152
  - This is by design — "用完即走"
153
+
154
+ ### Heartbeat — 自动查匹配
155
+
156
+ 如果 agent 有 heartbeat 机制(定期唤醒),每次 heartbeat 时:
157
+ 1. 检查用户是否有 Antenna profile(`antenna_profile` action="get")
158
+ 2. 如果有,调 `antenna_check_matches` 查看是否有新的 mutual match
159
+ 3. 如果有新匹配,主动通知用户:"有人想认识你!"并展示对方信息
160
+ 4. 如果没有新匹配,不用说话(静默跳过)
161
+
162
+ 这样用户不需要主动问,agent 会自动帮忙盯着。