antenna-openclaw-plugin 0.1.2 → 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 +15 -34
- package/package.json +1 -1
- package/skills/antenna/SKILL.md +24 -17
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
|
|
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({
|
|
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({
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
173
|
+
total: others.length,
|
|
174
|
+
radius_m: radius,
|
|
175
|
+
instruction: "根据你对用户的了解(记忆、偏好、最近的状态),判断哪些人值得推荐,为每个推荐写一句个性化的匹配理由。",
|
|
195
176
|
});
|
|
196
177
|
},
|
|
197
178
|
});
|
package/package.json
CHANGED
package/skills/antenna/SKILL.md
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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:
|