antenna-openclaw-plugin 0.3.2 → 0.4.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 +153 -1
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createClient, SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
+
import { execSync } from "child_process";
|
|
2
3
|
|
|
3
4
|
// ─── Built-in Supabase config (shared backend, zero config) ─────────
|
|
4
5
|
|
|
@@ -106,6 +107,90 @@ function ok(data: any) {
|
|
|
106
107
|
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
107
108
|
}
|
|
108
109
|
|
|
110
|
+
// ─── Cron helpers ────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
const FOLLOW_UP_INTERVAL_MS = 15 * 60 * 1000; // 15 minutes
|
|
113
|
+
const FOLLOW_UP_MAX_DURATION_MS = 2 * 60 * 60 * 1000; // 2 hours
|
|
114
|
+
const _activeFollowUps = new Map<string, string>(); // "deviceA→deviceB" → cronJobId
|
|
115
|
+
|
|
116
|
+
function cronJobId(deviceA: string, deviceB: string): string {
|
|
117
|
+
// Stable, filesystem-safe id
|
|
118
|
+
const safe = (s: string) => s.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
119
|
+
return `antenna-follow-${safe(deviceA)}-${safe(deviceB)}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function startFollowUpCron(
|
|
123
|
+
deviceId: string,
|
|
124
|
+
targetDeviceId: string,
|
|
125
|
+
channel: string,
|
|
126
|
+
userId: string,
|
|
127
|
+
targetName: string,
|
|
128
|
+
logger: any,
|
|
129
|
+
): void {
|
|
130
|
+
const jobId = cronJobId(deviceId, targetDeviceId);
|
|
131
|
+
const key = `${deviceId}→${targetDeviceId}`;
|
|
132
|
+
|
|
133
|
+
// Already tracking this pair
|
|
134
|
+
if (_activeFollowUps.has(key)) return;
|
|
135
|
+
|
|
136
|
+
const message = [
|
|
137
|
+
`[Antenna follow-up] 检查匹配状态:`,
|
|
138
|
+
`运行 antenna_check_matches(sender_id="${userId}", channel="${channel}")。`,
|
|
139
|
+
`如果 ${targetName}(${targetDeviceId})已经 mutual match,`,
|
|
140
|
+
`用 message tool 通知用户:「${targetName} 也接受了你的匹配!」并附上对方的联系方式(如果有)。`,
|
|
141
|
+
`然后运行:openclaw cron remove ${jobId}`,
|
|
142
|
+
`如果还没有 mutual,回复 HEARTBEAT_OK。`,
|
|
143
|
+
].join(" ");
|
|
144
|
+
|
|
145
|
+
// Schedule: expire after 2h using --at for auto-cleanup
|
|
146
|
+
const expiresAt = new Date(Date.now() + FOLLOW_UP_MAX_DURATION_MS).toISOString();
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
// Create recurring 15-min job
|
|
150
|
+
execSync(
|
|
151
|
+
`openclaw cron add` +
|
|
152
|
+
` --name "Antenna: 等待 ${targetName} 回应"` +
|
|
153
|
+
` --every 15m` +
|
|
154
|
+
` --session isolated` +
|
|
155
|
+
` --message ${JSON.stringify(message)}` +
|
|
156
|
+
` --announce` +
|
|
157
|
+
` --channel ${channel}` +
|
|
158
|
+
` --to "${userId}"`,
|
|
159
|
+
{ timeout: 10_000, encoding: "utf-8" },
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
_activeFollowUps.set(key, jobId);
|
|
163
|
+
logger.info(`Antenna: follow-up cron created for ${key} (job: ${jobId})`);
|
|
164
|
+
|
|
165
|
+
// Schedule auto-cleanup after 2 hours
|
|
166
|
+
setTimeout(() => {
|
|
167
|
+
try {
|
|
168
|
+
execSync(`openclaw cron remove ${jobId}`, { timeout: 5_000 });
|
|
169
|
+
logger.info(`Antenna: follow-up expired for ${key}`);
|
|
170
|
+
} catch {
|
|
171
|
+
// Job may already be removed
|
|
172
|
+
}
|
|
173
|
+
_activeFollowUps.delete(key);
|
|
174
|
+
}, FOLLOW_UP_MAX_DURATION_MS);
|
|
175
|
+
} catch (err: any) {
|
|
176
|
+
logger.warn(`Antenna: failed to create follow-up cron: ${err.message}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function stopFollowUpCron(deviceA: string, deviceB: string, logger: any): void {
|
|
181
|
+
const key = `${deviceA}→${deviceB}`;
|
|
182
|
+
const jobId = _activeFollowUps.get(key);
|
|
183
|
+
if (!jobId) return;
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
execSync(`openclaw cron remove ${jobId}`, { timeout: 5_000 });
|
|
187
|
+
logger.info(`Antenna: follow-up stopped for ${key}`);
|
|
188
|
+
} catch {
|
|
189
|
+
// Already removed
|
|
190
|
+
}
|
|
191
|
+
_activeFollowUps.delete(key);
|
|
192
|
+
}
|
|
193
|
+
|
|
109
194
|
// ─── Plugin ──────────────────────────────────────────────────────────
|
|
110
195
|
|
|
111
196
|
export default function register(api: any) {
|
|
@@ -233,6 +318,53 @@ export default function register(api: any) {
|
|
|
233
318
|
},
|
|
234
319
|
});
|
|
235
320
|
|
|
321
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
322
|
+
// Tool: antenna_checkin
|
|
323
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
324
|
+
api.registerTool({
|
|
325
|
+
name: "antenna_checkin",
|
|
326
|
+
description:
|
|
327
|
+
"Check in at a location — update your position so others can find you when they scan. Use when the user says 'I'm at XX' or wants to be discoverable without scanning others. Also works with place names (agent should geocode first).",
|
|
328
|
+
parameters: {
|
|
329
|
+
type: "object",
|
|
330
|
+
properties: {
|
|
331
|
+
lat: { type: "number", description: "Latitude" },
|
|
332
|
+
lng: { type: "number", description: "Longitude" },
|
|
333
|
+
sender_id: { type: "string", description: "The sender's user ID" },
|
|
334
|
+
channel: { type: "string", description: "The channel name" },
|
|
335
|
+
place_name: { type: "string", description: "Optional: name of the place (for confirmation message)" },
|
|
336
|
+
},
|
|
337
|
+
required: ["lat", "lng", "sender_id", "channel"],
|
|
338
|
+
},
|
|
339
|
+
async execute(_id: string, params: any) {
|
|
340
|
+
const cfg = getConfig(api);
|
|
341
|
+
const supabase = getSupabase(cfg);
|
|
342
|
+
const deviceId = deriveDeviceId(params.sender_id, params.channel);
|
|
343
|
+
const fuzzy = fuzzyCoords(params.lat, params.lng);
|
|
344
|
+
|
|
345
|
+
// Check if user has a profile first
|
|
346
|
+
const { data: profile } = await supabase.rpc("get_profile", { p_device_id: deviceId });
|
|
347
|
+
if (!profile) {
|
|
348
|
+
return ok({
|
|
349
|
+
checked_in: false,
|
|
350
|
+
message: "你还没有名片,别人看到你也不知道你是谁。先创建一个名片吧(告诉我你的名字、emoji、三句话介绍自己)。",
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const { error } = await supabase.rpc("upsert_profile_location", {
|
|
355
|
+
p_device_id: deviceId, p_lng: fuzzy.lng, p_lat: fuzzy.lat,
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
if (error) return ok({ error: error.message });
|
|
359
|
+
|
|
360
|
+
const place = params.place_name ? ` (${params.place_name})` : "";
|
|
361
|
+
return ok({
|
|
362
|
+
checked_in: true,
|
|
363
|
+
message: `已签到${place} 📍 现在附近的人扫描就能看到你了。`,
|
|
364
|
+
});
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
|
|
236
368
|
// ═══════════════════════════════════════════════════════════════════
|
|
237
369
|
// Tool: antenna_accept
|
|
238
370
|
// ═══════════════════════════════════════════════════════════════════
|
|
@@ -268,6 +400,10 @@ export default function register(api: any) {
|
|
|
268
400
|
);
|
|
269
401
|
|
|
270
402
|
if (reverse) {
|
|
403
|
+
// Mutual match! Stop any follow-up cron for this pair
|
|
404
|
+
stopFollowUpCron(deviceId, params.target_device_id, logger);
|
|
405
|
+
stopFollowUpCron(params.target_device_id, deviceId, logger);
|
|
406
|
+
|
|
271
407
|
return ok({
|
|
272
408
|
accepted: true, mutual: true,
|
|
273
409
|
their_contact: reverse.contact_info_a || null,
|
|
@@ -277,7 +413,19 @@ export default function register(api: any) {
|
|
|
277
413
|
});
|
|
278
414
|
}
|
|
279
415
|
|
|
280
|
-
|
|
416
|
+
// Not mutual yet — start a follow-up cron (check every 15min for 2h)
|
|
417
|
+
const { data: targetProfile } = await supabase.rpc("get_profile", { p_device_id: params.target_device_id });
|
|
418
|
+
const targetName = targetProfile?.display_name || "对方";
|
|
419
|
+
|
|
420
|
+
startFollowUpCron(
|
|
421
|
+
deviceId, params.target_device_id,
|
|
422
|
+
params.channel, params.sender_id, targetName, logger,
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
return ok({
|
|
426
|
+
accepted: true, mutual: false,
|
|
427
|
+
message: "已接受。我会在接下来 2 小时内每 15 分钟检查一次对方是否回应,有消息第一时间告诉你。",
|
|
428
|
+
});
|
|
281
429
|
},
|
|
282
430
|
});
|
|
283
431
|
|
|
@@ -319,6 +467,10 @@ export default function register(api: any) {
|
|
|
319
467
|
(m: any) => m.device_id_a === match.device_id_b
|
|
320
468
|
);
|
|
321
469
|
if (reverse) {
|
|
470
|
+
// Clean up follow-up crons for this mutual pair
|
|
471
|
+
stopFollowUpCron(deviceId, match.device_id_b, logger);
|
|
472
|
+
stopFollowUpCron(match.device_id_b, deviceId, logger);
|
|
473
|
+
|
|
322
474
|
const { data: profile } = await supabase.rpc("get_profile", { p_device_id: match.device_id_b });
|
|
323
475
|
mutualMatches.push({
|
|
324
476
|
device_id: match.device_id_b,
|