antenna-openclaw-plugin 1.2.16 → 1.2.18

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
@@ -48,6 +48,7 @@ let _supabaseUrl: string | null = null;
48
48
  const _lastScanTime = new Map<string, number>();
49
49
  const SCAN_DEBOUNCE_MS = 30_000;
50
50
  const _knownDeviceIds = new Set<string>();
51
+ const _channelContext = new Map<string, string>(); // device_id → chatId (e.g. discord channel ID)
51
52
 
52
53
  function getConfig(api: any): AntennaConfig {
53
54
  const cfg = api.config?.plugins?.entries?.antenna?.config ?? {};
@@ -122,23 +123,35 @@ function cronJobId(deviceA: string, deviceB: string): string {
122
123
  return `antenna-follow-${safe(deviceA)}-${safe(deviceB)}`;
123
124
  }
124
125
 
125
- /** Send a real-time notification to a user via openclaw agent --deliver */
126
+ /** Send a real-time notification to a user via openclaw message send */
126
127
  function notifyUser(
127
128
  channel: string,
128
129
  userId: string,
129
130
  message: string,
130
131
  logger: any,
131
132
  ): void {
133
+ const deviceId = `${channel}:${userId}`;
134
+ const chatId = _channelContext.get(deviceId);
135
+
132
136
  try {
133
- execSync(
134
- `openclaw agent` +
135
- ` --message ${JSON.stringify(message)}` +
136
- ` --deliver` +
137
- ` --agent main` +
138
- ` --to ${channel}:${userId}`,
139
- { timeout: 30_000, encoding: "utf-8" },
140
- );
141
- logger.info(`Antenna: notified ${channel}:${userId}`);
137
+ if (chatId) {
138
+ // Use message send with known chat context
139
+ execSync(
140
+ `openclaw message send --channel ${channel} --target ${chatId} -m ${JSON.stringify(message)}`,
141
+ { timeout: 30_000, encoding: "utf-8" },
142
+ );
143
+ } else {
144
+ // Fallback: try deliver
145
+ execSync(
146
+ `openclaw agent` +
147
+ ` --message ${JSON.stringify(message)}` +
148
+ ` --deliver` +
149
+ ` --agent main` +
150
+ ` --to ${channel}:${userId}`,
151
+ { timeout: 30_000, encoding: "utf-8" },
152
+ );
153
+ }
154
+ logger.info(`Antenna: notified ${channel}:${userId} (chat=${chatId || 'deliver'})`);
142
155
  } catch (err: any) {
143
156
  logger.warn(`Antenna: notify failed for ${channel}:${userId}: ${err.message}`);
144
157
  }
@@ -1198,6 +1211,82 @@ export default function register(api: any) {
1198
1211
  logger.warn("Antenna: failed to start realtime subscription, falling back to poll only:", err.message);
1199
1212
  }
1200
1213
 
1214
+ // ── Supabase Realtime: event participant notifications ──────
1215
+ try {
1216
+ const epCfg = getConfig(api);
1217
+ const epSb = getSupabase(epCfg);
1218
+ epSb
1219
+ .channel('antenna-event-notify')
1220
+ .on('postgres_changes',
1221
+ { event: 'INSERT', schema: 'public', table: 'event_participants' },
1222
+ async (payload: any) => {
1223
+ try {
1224
+ // New participant joined (pending) → notify creator
1225
+ if (payload.new?.status !== 'pending') return;
1226
+ const eventId = payload.new?.event_id;
1227
+ const applicantDeviceId = payload.new?.device_id;
1228
+ if (!eventId || !applicantDeviceId) return;
1229
+
1230
+ // Get event info via RPC
1231
+ const { data: event } = await epSb.rpc('get_event_by_id', { p_event_id: eventId });
1232
+ if (!event?.found || !event?.notify_on_join || !event?.created_by) return;
1233
+
1234
+ // Get applicant profile
1235
+ const { data: applicant } = await epSb.rpc('get_profile', { p_device_id: applicantDeviceId });
1236
+ const aName = applicant?.display_name || '某人';
1237
+ const aEmoji = applicant?.emoji || '👤';
1238
+
1239
+ const parts = event.created_by.split(':');
1240
+ if (parts.length < 2) return;
1241
+ notifyUser(parts[0], parts.slice(1).join(':'),
1242
+ `[Antenna] 📩 ${aEmoji} ${aName} 申请加入你的活动「${event.name}」\n\n用 antenna_event_scan --code ${event.code} 查看申请者名片并审批。`,
1243
+ logger);
1244
+ } catch (err: any) {
1245
+ logger.warn('Antenna: event participant INSERT handler error:', err.message);
1246
+ }
1247
+ }
1248
+ )
1249
+ .on('postgres_changes',
1250
+ { event: 'UPDATE', schema: 'public', table: 'event_participants' },
1251
+ async (payload: any) => {
1252
+ try {
1253
+ // Status changed → notify the participant
1254
+ const oldStatus = payload.old?.status;
1255
+ const newStatus = payload.new?.status;
1256
+ if (!oldStatus || oldStatus === newStatus) return;
1257
+ if (newStatus !== 'active' && newStatus !== 'rejected') return;
1258
+
1259
+ const participantDeviceId = payload.new?.device_id;
1260
+ const eventId = payload.new?.event_id;
1261
+ if (!participantDeviceId || !eventId) return;
1262
+
1263
+ const { data: event } = await epSb.rpc('get_event_by_id', { p_event_id: eventId });
1264
+ const eventName = event?.name || '活动';
1265
+
1266
+ const parts = participantDeviceId.split(':');
1267
+ if (parts.length < 2) return;
1268
+
1269
+ if (newStatus === 'active') {
1270
+ notifyUser(parts[0], parts.slice(1).join(':'),
1271
+ `[Antenna] ✅ 你的申请已通过!欢迎加入「${eventName}」\n\n用 antenna_event_scan --code ${event?.code} 查看其他参与者。`,
1272
+ logger);
1273
+ } else if (newStatus === 'rejected') {
1274
+ notifyUser(parts[0], parts.slice(1).join(':'),
1275
+ `[Antenna] ❌ 你的申请未通过「${eventName}」的审核。`,
1276
+ logger);
1277
+ }
1278
+ } catch (err: any) {
1279
+ logger.warn('Antenna: event participant UPDATE handler error:', err.message);
1280
+ }
1281
+ }
1282
+ )
1283
+ .subscribe((status: string) => {
1284
+ logger.info(`Antenna: event participant realtime status: ${status}`);
1285
+ });
1286
+ } catch (err: any) {
1287
+ logger.warn('Antenna: failed to start event participant realtime:', err.message);
1288
+ }
1289
+
1201
1290
  // ── Poll fallback: catch anything Realtime missed ───────────
1202
1291
  _pollTimer = setInterval(async () => {
1203
1292
  try {
@@ -1315,6 +1404,16 @@ export default function register(api: any) {
1315
1404
  const cfg = getConfig(api);
1316
1405
  let hint = "";
1317
1406
 
1407
+ // --- Track chat context for notifications ---
1408
+ const senderId = ctx?.SenderId || ctx?.senderId;
1409
+ const ch = ctx?.Channel || ctx?.channel;
1410
+ const chatId = ctx?.ChatId || ctx?.chatId || ctx?.chat_id;
1411
+ if (senderId && ch && chatId) {
1412
+ const deviceId = `${ch}:${senderId}`;
1413
+ _channelContext.set(deviceId, chatId);
1414
+ _knownDeviceIds.add(deviceId);
1415
+ }
1416
+
1318
1417
  // --- Auto-scan on location ---
1319
1418
  if (cfg.autoScanOnLocation === false) return {};
1320
1419
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antenna-openclaw-plugin",
3
- "version": "1.2.16",
3
+ "version": "1.2.18",
4
4
  "description": "Antenna — agent-mediated nearby people discovery for OpenClaw",
5
5
  "openclaw": {
6
6
  "extensions": ["./index.ts"]
@@ -90,8 +90,13 @@ Generate a GPS link for setting event location.
90
90
 
91
91
  ### When someone shares an event link
92
92
  1. Extract the code from `antenna.fyi/events/CODE`
93
- 2. Call `antenna_event_join(code)` — this will auto-check in if applicable
94
- 3. If join fails with "Create a profile first", guide profile creation then retry
93
+ 2. Call `antenna_event_join(code)` — this checks everything:
94
+ - If no profile "Create a profile first"
95
+ - If event requires approval and no `application_context` provided → returns `needs_screening: true` + `screening_questions` array
96
+ - If screening questions returned: **ask the user each question**, collect answers, then call `antenna_event_join(code, application_context="collected answers")` again
97
+ - If join succeeds with `status: pending` → tell user "waiting for organizer approval"
98
+ - If join succeeds with `status: active` → user is in!
99
+ 3. Auto check-in happens automatically if event has started + GPS within 1km
95
100
 
96
101
  ### When someone says "who's here" at an event
97
102
  1. Call `antenna_event_scan(code)`