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 +15 -34
- package/package.json +1 -1
- package/skills/antenna/SKILL.md +67 -22
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"
|
|
@@ -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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
>
|
|
69
|
-
|
|
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
|
-
>
|
|
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:
|