antenna-openclaw-plugin 0.1.0 → 0.1.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 +83 -278
- package/package.json +1 -1
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;
|
|
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
|
|
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
|
-
|
|
146
|
-
|
|
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
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
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
|
-
|
|
281
|
-
|
|
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
|
-
|
|
289
|
-
};
|
|
194
|
+
total_nearby: others.length, radius_m: radius,
|
|
195
|
+
});
|
|
290
196
|
},
|
|
291
197
|
});
|
|
292
198
|
|
|
293
199
|
// ═══════════════════════════════════════════════════════════════════
|
|
294
|
-
// Tool: antenna_profile
|
|
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
|
-
|
|
314
|
-
|
|
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
|
-
|
|
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
|
-
|
|
363
|
-
|
|
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
|
-
|
|
377
|
-
|
|
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
|
-
|
|
391
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
458
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
497
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
},
|