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 +109 -10
- package/package.json +1 -1
- package/skills/antenna/EVENTS.md +7 -2
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
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
package/skills/antenna/EVENTS.md
CHANGED
|
@@ -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
|
|
94
|
-
|
|
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)`
|