antenna-openclaw-plugin 0.1.0 → 0.1.2

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
@@ -42,13 +42,10 @@ interface MatchResult {
42
42
 
43
43
  // ─── Helpers ─────────────────────────────────────────────────────────
44
44
 
45
- // Cached Supabase client (singleton per config)
46
45
  let _supabaseClient: SupabaseClient | null = null;
47
46
  let _supabaseUrl: string | null = null;
48
-
49
- // Rate limiting: track last scan time per device_id
50
47
  const _lastScanTime = new Map<string, number>();
51
- const SCAN_DEBOUNCE_MS = 30_000; // 30 seconds
48
+ const SCAN_DEBOUNCE_MS = 30_000;
52
49
 
53
50
  function getConfig(api: any): AntennaConfig {
54
51
  const cfg = api.config?.plugins?.entries?.antenna?.config ?? {};
@@ -64,9 +61,7 @@ function getConfig(api: any): AntennaConfig {
64
61
 
65
62
  function getSupabase(cfg: AntennaConfig): SupabaseClient {
66
63
  const url = cfg.supabaseUrl!;
67
- if (_supabaseClient && _supabaseUrl === url) {
68
- return _supabaseClient;
69
- }
64
+ if (_supabaseClient && _supabaseUrl === url) return _supabaseClient;
70
65
  _supabaseClient = createClient(url, cfg.supabaseKey!);
71
66
  _supabaseUrl = url;
72
67
  return _supabaseClient;
@@ -75,9 +70,7 @@ function getSupabase(cfg: AntennaConfig): SupabaseClient {
75
70
  function isRateLimited(deviceId: string): boolean {
76
71
  const now = Date.now();
77
72
  const last = _lastScanTime.get(deviceId);
78
- if (last && now - last < SCAN_DEBOUNCE_MS) {
79
- return true;
80
- }
73
+ if (last && now - last < SCAN_DEBOUNCE_MS) return true;
81
74
  _lastScanTime.set(deviceId, now);
82
75
  if (_lastScanTime.size > 1000) {
83
76
  for (const [k, v] of _lastScanTime) {
@@ -87,50 +80,38 @@ function isRateLimited(deviceId: string): boolean {
87
80
  return false;
88
81
  }
89
82
 
90
- /**
91
- * Snap coordinates to ~150m precision (geohash-like rounding).
92
- * lat: round to 3 decimal places (~111m)
93
- * lng: round to 3 decimal places (~85-111m depending on latitude)
94
- */
95
- function fuzzyCoords(lat: number, lng: number): { lat: number; lng: number } {
83
+ function fuzzyCoords(lat: number, lng: number) {
96
84
  return {
97
85
  lat: Math.round(lat * 1000) / 1000,
98
86
  lng: Math.round(lng * 1000) / 1000,
99
87
  };
100
88
  }
101
89
 
102
- /**
103
- * Extract keywords from profile lines.
104
- * TODO: Replace with LLM-based matching for better Chinese support.
105
- * Current approach: split on punctuation/whitespace, keep tokens > 1 char.
106
- * Works OK for English and simple Chinese phrases, but can't handle
107
- * semantic similarity (e.g. "跑步" vs "慢跑").
108
- */
90
+ // TODO: Replace with LLM-based matching for better Chinese support
109
91
  function extractWords(profile: Partial<Profile>): string[] {
110
92
  const text = [profile.line1, profile.line2, profile.line3]
111
93
  .filter(Boolean)
112
94
  .join(" ")
113
95
  .toLowerCase();
114
- return text
115
- .split(/[\s,,。.!!??、;;::]+/)
116
- .filter((w) => w.length > 1);
96
+ return text.split(/[\s,,。.!!??、;;::]+/).filter((w) => w.length > 1);
117
97
  }
118
98
 
119
- /**
120
- * Generate a stable device_id from senderId + channel.
121
- * This maps a chat user to a unique Antenna identity.
122
- */
123
99
  function deriveDeviceId(senderId: string, channel: string): string {
124
100
  return `${channel}:${senderId}`;
125
101
  }
126
102
 
103
+ /** Wrap result as MCP tool response */
104
+ function ok(data: any) {
105
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
106
+ }
107
+
127
108
  // ─── Plugin ──────────────────────────────────────────────────────────
128
109
 
129
110
  export default function register(api: any) {
130
111
  const logger = api.logger;
131
112
 
132
113
  // ═══════════════════════════════════════════════════════════════════
133
- // Tool: antenna_scan — scan nearby people from a location
114
+ // Tool: antenna_scan
134
115
  // ═══════════════════════════════════════════════════════════════════
135
116
  api.registerTool({
136
117
  name: "antenna_scan",
@@ -141,157 +122,82 @@ export default function register(api: any) {
141
122
  properties: {
142
123
  lat: { type: "number", description: "Latitude" },
143
124
  lng: { type: "number", description: "Longitude" },
144
- radius_m: {
145
- type: "number",
146
- description: "Search radius in meters (default: 500)",
147
- },
148
- sender_id: {
149
- type: "string",
150
- description: "The sender's user ID (from message context)",
151
- },
152
- channel: {
153
- type: "string",
154
- description: "The channel name (telegram, whatsapp, etc.)",
155
- },
125
+ radius_m: { type: "number", description: "Search radius in meters (default: 500)" },
126
+ sender_id: { type: "string", description: "The sender's user ID (from message context)" },
127
+ channel: { type: "string", description: "The channel name (telegram, whatsapp, etc.)" },
156
128
  },
157
129
  required: ["lat", "lng", "sender_id", "channel"],
158
130
  },
159
- handler: async (params: {
160
- lat: number;
161
- lng: number;
162
- radius_m?: number;
163
- sender_id: string;
164
- channel: string;
165
- }) => {
131
+ async execute(_id: string, params: any) {
166
132
  const cfg = getConfig(api);
167
133
  const supabase = getSupabase(cfg);
168
134
  const deviceId = deriveDeviceId(params.sender_id, params.channel);
169
135
  const radius = params.radius_m ?? cfg.defaultRadiusM ?? 500;
170
136
  const maxMatches = cfg.maxMatches ?? 5;
171
137
 
172
- // Rate limit: skip if scanned less than 30s ago
173
138
  if (isRateLimited(deviceId)) {
174
- return {
175
- matches: [],
176
- message: "刚刚才扫描过,稍等一会儿再试。",
177
- rate_limited: true,
178
- };
139
+ return ok({ matches: [], message: "刚刚才扫描过,稍等一会儿再试。", rate_limited: true });
179
140
  }
180
141
 
181
- // Fuzzy coordinates for privacy (~150m precision)
182
142
  const fuzzy = fuzzyCoords(params.lat, params.lng);
183
143
 
184
- // Update my location using PostGIS RPC
185
- const { error: upsertErr } = await supabase.rpc(
186
- "upsert_profile_location",
187
- {
188
- p_device_id: deviceId,
189
- p_lng: fuzzy.lng,
190
- p_lat: fuzzy.lat,
191
- }
192
- );
193
-
144
+ const { error: upsertErr } = await supabase.rpc("upsert_profile_location", {
145
+ p_device_id: deviceId, p_lng: fuzzy.lng, p_lat: fuzzy.lat,
146
+ });
194
147
  if (upsertErr) {
195
148
  logger.warn("Antenna: upsert_profile_location failed:", upsertErr.message);
196
- // Fallback: upsert without location
197
- await supabase.from("profiles").upsert(
198
- {
199
- device_id: deviceId,
200
- last_seen_at: new Date().toISOString(),
201
- visible: true,
202
- },
203
- { onConflict: "device_id" }
204
- );
205
149
  }
206
150
 
207
- // Query nearby (use original coords for better accuracy in query)
208
151
  const { data: nearby, error } = await supabase.rpc("nearby_profiles", {
209
- p_lat: fuzzy.lat,
210
- p_lng: fuzzy.lng,
211
- p_radius_m: radius,
152
+ p_lat: fuzzy.lat, p_lng: fuzzy.lng, p_radius_m: radius,
212
153
  });
213
154
 
214
- if (error) {
215
- return { error: error.message };
216
- }
155
+ if (error) return ok({ error: error.message });
217
156
 
218
- const others = (nearby ?? []).filter(
219
- (p: Profile) => p.device_id !== deviceId
220
- );
157
+ const others = (nearby ?? []).filter((p: Profile) => p.device_id !== deviceId);
221
158
 
222
159
  if (others.length === 0) {
223
- return {
224
- matches: [],
225
- message: `在 ${radius}m 范围内没有发现其他人。试试扩大范围?`,
226
- };
160
+ return ok({ matches: [], message: `在 ${radius}m 范围内没有发现其他人。试试扩大范围?` });
227
161
  }
228
162
 
229
- // Get my profile for matching via RPC
230
- const { data: myProfile } = await supabase.rpc("get_profile", {
231
- p_device_id: deviceId,
232
- });
163
+ const { data: myProfile } = await supabase.rpc("get_profile", { p_device_id: deviceId });
233
164
 
234
- // Score matches
235
165
  const myWords = myProfile ? extractWords(myProfile) : [];
236
166
  const scored: MatchResult[] = others.map((p: Profile) => {
237
167
  const theirWords = extractWords(p);
238
168
  const overlap = myWords.filter((w: string) => theirWords.includes(w));
239
- const score =
240
- myWords.length > 0
241
- ? Math.min(overlap.length / myWords.length, 1)
242
- : 0;
243
- const reason =
244
- overlap.length > 0
245
- ? `你们都提到了 ${overlap.slice(0, 3).join("、")}——可能聊得来`
246
- : `${p.display_name || p.emoji || "TA"} 就在附近`;
247
- return {
248
- device_id: p.device_id,
249
- display_name: p.display_name,
250
- emoji: p.emoji,
251
- line1: p.line1,
252
- line2: p.line2,
253
- line3: p.line3,
254
- score,
255
- reason,
256
- };
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 };
257
175
  });
258
176
 
259
177
  scored.sort((a, b) => b.score - a.score);
260
178
  const topMatches = scored.slice(0, maxMatches);
261
179
 
262
- // Store matches
263
180
  const expiryHours = cfg.matchExpiryHours ?? 24;
264
-
265
- // Store matches via RPC (SECURITY DEFINER, works with anon key)
266
181
  for (const m of topMatches) {
267
182
  await supabase.rpc("upsert_match", {
268
- p_device_id_a: deviceId,
269
- p_device_id_b: m.device_id,
270
- p_reason: m.reason,
271
- p_score: m.score,
272
- p_status: "pending",
273
- p_expires_hours: expiryHours,
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,
274
185
  });
275
186
  }
276
187
 
277
- return {
188
+ return ok({
278
189
  matches: topMatches.map((m) => ({
279
- emoji: m.emoji || "👤",
280
- name: m.display_name || "匿名",
281
- line1: m.line1,
282
- line2: m.line2,
283
- line3: m.line3,
284
- score: m.score,
285
- reason: m.reason,
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,
286
193
  })),
287
- total_nearby: others.length,
288
- radius_m: radius,
289
- };
194
+ total_nearby: others.length, radius_m: radius,
195
+ });
290
196
  },
291
197
  });
292
198
 
293
199
  // ═══════════════════════════════════════════════════════════════════
294
- // Tool: antenna_profile — view or update my profile (name card)
200
+ // Tool: antenna_profile
295
201
  // ═══════════════════════════════════════════════════════════════════
296
202
  api.registerTool({
297
203
  name: "antenna_profile",
@@ -300,106 +206,54 @@ export default function register(api: any) {
300
206
  parameters: {
301
207
  type: "object",
302
208
  properties: {
303
- action: {
304
- type: "string",
305
- enum: ["get", "set"],
306
- description: "'get' to view profile, 'set' to update it",
307
- },
209
+ action: { type: "string", enum: ["get", "set"], description: "'get' to view profile, 'set' to update it" },
308
210
  sender_id: { type: "string", description: "The sender's user ID" },
309
211
  channel: { type: "string", description: "The channel name" },
310
212
  display_name: { type: "string", description: "Display name" },
311
213
  emoji: { type: "string", description: "Profile emoji" },
312
- line1: {
313
- type: "string",
314
- description: "First line (who you are / what you do)",
315
- },
316
- line2: {
317
- type: "string",
318
- description: "Second line (what you're into)",
319
- },
320
- line3: {
321
- type: "string",
322
- description: "Third line (what you're looking for)",
323
- },
324
- visible: {
325
- type: "boolean",
326
- description: "Whether to be visible to others",
327
- },
214
+ line1: { type: "string", description: "First line (who you are / what you do)" },
215
+ line2: { type: "string", description: "Second line (what you're into)" },
216
+ line3: { type: "string", description: "Third line (what you're looking for)" },
217
+ visible: { type: "boolean", description: "Whether to be visible to others" },
328
218
  },
329
219
  required: ["action", "sender_id", "channel"],
330
220
  },
331
- handler: async (params: {
332
- action: string;
333
- sender_id: string;
334
- channel: string;
335
- display_name?: string;
336
- emoji?: string;
337
- line1?: string;
338
- line2?: string;
339
- line3?: string;
340
- visible?: boolean;
341
- }) => {
221
+ async execute(_id: string, params: any) {
342
222
  const cfg = getConfig(api);
343
223
  const supabase = getSupabase(cfg);
344
224
  const deviceId = deriveDeviceId(params.sender_id, params.channel);
345
225
 
346
226
  if (params.action === "get") {
347
- const { data, error } = await supabase.rpc("get_profile", {
348
- p_device_id: deviceId,
349
- });
350
-
227
+ const { data, error } = await supabase.rpc("get_profile", { p_device_id: deviceId });
351
228
  if (error || !data) {
352
- return {
353
- exists: false,
354
- message:
355
- "你还没有名片。告诉我你的名字、一个 emoji、和三句话介绍自己,我帮你创建。",
356
- };
229
+ return ok({ exists: false, message: "你还没有名片。告诉我你的名字、一个 emoji、和三句话介绍自己,我帮你创建。" });
357
230
  }
358
-
359
- return {
231
+ return ok({
360
232
  exists: true,
361
- profile: {
362
- display_name: data.display_name,
363
- emoji: data.emoji,
364
- line1: data.line1,
365
- line2: data.line2,
366
- line3: data.line3,
367
- visible: data.visible,
368
- },
369
- };
233
+ profile: { display_name: data.display_name, emoji: data.emoji,
234
+ line1: data.line1, line2: data.line2, line3: data.line3, visible: data.visible },
235
+ });
370
236
  }
371
237
 
372
- // action === 'set' — use RPC for write (SECURITY DEFINER)
373
238
  const { data, error } = await supabase.rpc("upsert_profile", {
374
239
  p_device_id: deviceId,
375
- p_display_name: params.display_name ?? null,
376
- p_emoji: params.emoji ?? null,
377
- p_line1: params.line1 ?? null,
378
- p_line2: params.line2 ?? null,
379
- p_line3: params.line3 ?? null,
380
- p_visible: params.visible ?? true,
240
+ p_display_name: params.display_name ?? null, p_emoji: params.emoji ?? null,
241
+ p_line1: params.line1 ?? null, p_line2: params.line2 ?? null,
242
+ p_line3: params.line3 ?? null, p_visible: params.visible ?? true,
381
243
  });
382
244
 
383
- if (error) {
384
- return { error: error.message };
385
- }
245
+ if (error) return ok({ error: error.message });
386
246
 
387
- return {
247
+ return ok({
388
248
  updated: true,
389
- profile: {
390
- display_name: data.display_name,
391
- emoji: data.emoji,
392
- line1: data.line1,
393
- line2: data.line2,
394
- line3: data.line3,
395
- visible: data.visible,
396
- },
397
- };
249
+ profile: { display_name: data.display_name, emoji: data.emoji,
250
+ line1: data.line1, line2: data.line2, line3: data.line3, visible: data.visible },
251
+ });
398
252
  },
399
253
  });
400
254
 
401
255
  // ═══════════════════════════════════════════════════════════════════
402
- // Tool: antenna_accept — accept a match and optionally share contact
256
+ // Tool: antenna_accept
403
257
  // ═══════════════════════════════════════════════════════════════════
404
258
  api.registerTool({
405
259
  name: "antenna_accept",
@@ -410,71 +264,44 @@ export default function register(api: any) {
410
264
  properties: {
411
265
  sender_id: { type: "string" },
412
266
  channel: { type: "string" },
413
- target_device_id: {
414
- type: "string",
415
- description: "The device_id of the person to accept",
416
- },
417
- contact_info: {
418
- type: "string",
419
- description:
420
- "Optional contact info to share (e.g. 'WeChat: yi_xxx' or 'Telegram: @yi')",
421
- },
267
+ target_device_id: { type: "string", description: "The device_id of the person to accept" },
268
+ contact_info: { type: "string", description: "Optional contact info to share (e.g. 'WeChat: yi_xxx')" },
422
269
  },
423
270
  required: ["sender_id", "channel", "target_device_id"],
424
271
  },
425
- handler: async (params: {
426
- sender_id: string;
427
- channel: string;
428
- target_device_id: string;
429
- contact_info?: string;
430
- }) => {
272
+ async execute(_id: string, params: any) {
431
273
  const cfg = getConfig(api);
432
274
  const supabase = getSupabase(cfg);
433
275
  const deviceId = deriveDeviceId(params.sender_id, params.channel);
434
276
 
435
- // Update match status + optional contact info via RPC
436
277
  const { error } = await supabase.rpc("upsert_match", {
437
- p_device_id_a: deviceId,
438
- p_device_id_b: params.target_device_id,
439
- p_status: "accepted",
440
- p_contact_info: params.contact_info ?? null,
278
+ p_device_id_a: deviceId, p_device_id_b: params.target_device_id,
279
+ p_status: "accepted", p_contact_info: params.contact_info ?? null,
441
280
  });
442
281
 
443
- if (error) {
444
- return { error: error.message };
445
- }
446
-
447
- // Check if mutual match via RPC
448
- const { data: myMatches } = await supabase.rpc("get_my_matches", {
449
- p_device_id: deviceId,
450
- });
282
+ if (error) return ok({ error: error.message });
451
283
 
284
+ const { data: myMatches } = await supabase.rpc("get_my_matches", { p_device_id: deviceId });
452
285
  const reverse = (myMatches || []).find(
453
286
  (m: any) => m.device_id_a === params.target_device_id && m.device_id_b === deviceId
454
287
  );
455
288
 
456
289
  if (reverse) {
457
- // Mutual match! Return the other person's contact info if they shared it
458
- return {
459
- accepted: true,
460
- mutual: true,
290
+ return ok({
291
+ accepted: true, mutual: true,
461
292
  their_contact: reverse.contact_info_a || null,
462
293
  message: reverse.contact_info_a
463
294
  ? `双方都接受了!对方分享的联系方式:${reverse.contact_info_a}`
464
295
  : "双方都接受了!但对方还没有分享联系方式,等 TA 分享后会通知你。",
465
- };
296
+ });
466
297
  }
467
298
 
468
- return {
469
- accepted: true,
470
- mutual: false,
471
- message: "已接受。等对方也接受后,你们就可以交换联系方式了。",
472
- };
299
+ return ok({ accepted: true, mutual: false, message: "已接受。等对方也接受后,你们就可以交换联系方式了。" });
473
300
  },
474
301
  });
475
302
 
476
303
  // ═══════════════════════════════════════════════════════════════════
477
- // Tool: antenna_check_matches — check for mutual matches / new contact info
304
+ // Tool: antenna_check_matches
478
305
  // ═══════════════════════════════════════════════════════════════════
479
306
  api.registerTool({
480
307
  name: "antenna_check_matches",
@@ -488,54 +315,37 @@ export default function register(api: any) {
488
315
  },
489
316
  required: ["sender_id", "channel"],
490
317
  },
491
- handler: async (params: { sender_id: string; channel: string }) => {
318
+ async execute(_id: string, params: any) {
492
319
  const cfg = getConfig(api);
493
320
  const supabase = getSupabase(cfg);
494
321
  const deviceId = deriveDeviceId(params.sender_id, params.channel);
495
322
 
496
- // Find my accepted matches via RPC
497
- const { data: allMatches } = await supabase.rpc("get_my_matches", {
498
- p_device_id: deviceId,
499
- });
500
-
501
- const myMatches = (allMatches || []).filter(
502
- (m: any) => m.device_id_a === deviceId
503
- );
323
+ const { data: allMatches } = await supabase.rpc("get_my_matches", { p_device_id: deviceId });
324
+ const myMatches = (allMatches || []).filter((m: any) => m.device_id_a === deviceId);
504
325
 
505
326
  if (myMatches.length === 0) {
506
- return { mutual_matches: [], message: "目前没有进行中的匹配。" };
327
+ return ok({ mutual_matches: [], message: "目前没有进行中的匹配。" });
507
328
  }
508
329
 
509
- // Check which ones are mutual
510
330
  const mutualMatches = [];
511
331
  for (const match of myMatches) {
512
332
  const reverse = (allMatches || []).find(
513
333
  (m: any) => m.device_id_a === match.device_id_b && m.device_id_b === deviceId
514
334
  );
515
-
516
335
  if (reverse) {
517
- // Get their profile via RPC
518
- const { data: profile } = await supabase.rpc("get_profile", {
519
- p_device_id: match.device_id_b,
520
- });
521
-
336
+ const { data: profile } = await supabase.rpc("get_profile", { p_device_id: match.device_id_b });
522
337
  mutualMatches.push({
523
- name: profile?.display_name || "匿名",
524
- emoji: profile?.emoji || "👤",
525
- their_contact: reverse.contact_info_a || null,
526
- you_shared: match.contact_info_a || null,
338
+ name: profile?.display_name || "匿名", emoji: profile?.emoji || "👤",
339
+ their_contact: reverse.contact_info_a || null, you_shared: match.contact_info_a || null,
527
340
  });
528
341
  }
529
342
  }
530
343
 
531
344
  if (mutualMatches.length === 0) {
532
- return {
533
- mutual_matches: [],
534
- message: "你接受了一些匹配,但对方还没有回应。耐心等等 ⏳",
535
- };
345
+ return ok({ mutual_matches: [], message: "你接受了一些匹配,但对方还没有回应。耐心等等 ⏳" });
536
346
  }
537
347
 
538
- return { mutual_matches: mutualMatches };
348
+ return ok({ mutual_matches: mutualMatches });
539
349
  },
540
350
  });
541
351
 
@@ -549,23 +359,18 @@ export default function register(api: any) {
549
359
  const cfg = getConfig(api);
550
360
  if (cfg.autoScanOnLocation === false) return {};
551
361
 
552
- // Check if the inbound message has location context
553
362
  const lat = ctx?.LocationLat;
554
363
  const lon = ctx?.LocationLon;
555
364
  if (lat == null || lon == null) return {};
556
365
 
557
- // Inject a hint so the agent knows to use antenna_scan
558
366
  const isLive = ctx?.LocationIsLive ?? false;
559
367
  const locationName = ctx?.LocationName ?? "";
560
368
  const hint = isLive
561
369
  ? `\n\n[Antenna] 📡 收到实时位置 (${lat.toFixed(4)}, ${lon.toFixed(4)})${locationName ? ` — ${locationName}` : ""}。请使用 antenna_scan 工具查看附近有谁。参数:lat=${lat}, lng=${lon}, sender_id 和 channel 从消息上下文获取。`
562
370
  : `\n\n[Antenna] 📍 收到位置 (${lat.toFixed(4)}, ${lon.toFixed(4)})${locationName ? ` — ${locationName}` : ""}。请使用 antenna_scan 工具查看附近有谁。参数:lat=${lat}, lng=${lon}, sender_id 和 channel 从消息上下文获取。`;
563
371
 
564
- return {
565
- prependContext: hint,
566
- };
372
+ return { prependContext: hint };
567
373
  } catch {
568
- // Plugin not configured — silently skip
569
374
  return {};
570
375
  }
571
376
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antenna-openclaw-plugin",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Antenna — agent-mediated nearby people discovery for OpenClaw",
5
5
  "openclaw": {
6
6
  "extensions": ["./index.ts"]
@@ -50,11 +50,49 @@ Check for mutual matches and contact info updates.
50
50
 
51
51
  ## Behavior guidelines
52
52
 
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
53
+ ### First-time user — 聊天式引导(不要让用户填表)
54
+
55
+ **绝对不要**一次性说"请填写 emoji、名字、三句话介绍"——这会让用户懵掉。
56
+
57
+ 用聊天的方式一步一步引导:
58
+
59
+ **第一步:开场**
60
+ > "嘿,第一次用 Antenna?我帮你做张名片,附近的人会看到它。先聊几句就行。"
61
+
62
+ **第二步:问职业/身份**(→ line1)
63
+ > "你平时做什么?工作、学生、自由职业、还是别的?"
64
+
65
+ 用户可能回答很长,也可能很短。不管怎样,你提炼成一句简短的话。
66
+
67
+ **第三步:问兴趣**(→ line2)
68
+ > "最近在玩什么?或者对什么特别感兴趣?"
69
+
70
+ **第四步:问意图**(→ line3)
71
+ > "来这儿想认识什么样的人?或者找什么?"
72
+
73
+ **第五步:问名字和 emoji**
74
+ > "最后——你想被叫什么?再选个 emoji 代表你自己。"
75
+
76
+ **第六步:确认**
77
+ 把名片组装好,展示给用户确认:
78
+ > 你的名片:
79
+ >
80
+ > 🎸 **小林**
81
+ > 吉他手,在乐队弹后摇
82
+ > 喜欢 shoegaze 和 post-rock,最近在听 Mogwai
83
+ > 找人一起 jam 或者聊音乐
84
+ >
85
+ > 看看有没有要改的?OK 的话我就存了。
86
+
87
+ 用户说 OK → `antenna_profile` action="set" 保存。
88
+ 用户说要改 → 改完再确认。
89
+
90
+ **关键原则:**
91
+ - 每次只问一个问题
92
+ - 用户说的原话尽量保留,不要过度润色
93
+ - 可以帮用户缩短太长的回答,但要让用户确认
94
+ - 如果用户不想回答某一项,留空也行("那这行先空着,以后想加再说")
95
+ - 整个过程应该像跟朋友聊天,不像填表
58
96
 
59
97
  ### Showing results
60
98
  Present matches conversationally, not as a data dump: