antenna-openclaw-plugin 1.3.28 → 1.3.30
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 +419 -263
- package/migrations/drift_bottles.sql +177 -0
- package/openclaw.plugin.json +2 -2
- package/package.json +1 -1
- package/skills/antenna/SKILL.md +271 -228
- package/tests/drift-bottle.test.ts +109 -0
- package/skills/antenna/EVENTS.md +0 -163
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Drift Bottle Tests
|
|
2
|
+
// Run with: node --experimental-strip-types tests/drift-bottle.test.ts
|
|
3
|
+
// Requires the drift_bottles migration to be applied to Supabase first.
|
|
4
|
+
|
|
5
|
+
import { createClient } from "@supabase/supabase-js";
|
|
6
|
+
|
|
7
|
+
const SUPABASE_URL = "https://bcudjloikmpcqwcptuyd.supabase.co";
|
|
8
|
+
const SUPABASE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImJjdWRqbG9pa21wY3F3Y3B0dXlkIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzQ0MTg1NDgsImV4cCI6MjA4OTk5NDU0OH0.FaoC3QfpfHP1npNGjRchJAoAp2PdZtQe_WhP-t-GN1o";
|
|
9
|
+
|
|
10
|
+
const sb = createClient(SUPABASE_URL, SUPABASE_KEY);
|
|
11
|
+
|
|
12
|
+
const DEVICE_A = "test:user_alice_" + Date.now();
|
|
13
|
+
const DEVICE_B = "test:user_bob_" + Date.now();
|
|
14
|
+
|
|
15
|
+
let bottleId: string;
|
|
16
|
+
|
|
17
|
+
async function test(name: string, fn: () => Promise<void>) {
|
|
18
|
+
try {
|
|
19
|
+
await fn();
|
|
20
|
+
console.log(`✅ ${name}`);
|
|
21
|
+
} catch (e: any) {
|
|
22
|
+
console.error(`❌ ${name}: ${e.message}`);
|
|
23
|
+
process.exitCode = 1;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function assert(condition: boolean, msg: string) {
|
|
28
|
+
if (!condition) throw new Error(msg);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
await test("throw a bottle", async () => {
|
|
32
|
+
const { data, error } = await sb.rpc("throw_drift_bottle", {
|
|
33
|
+
p_device_id: DEVICE_A,
|
|
34
|
+
p_message: "你好,陌生人!这是一个测试漂流瓶 🌊",
|
|
35
|
+
});
|
|
36
|
+
assert(!error, `RPC error: ${error?.message}`);
|
|
37
|
+
assert(data.bottle_id != null, "should return bottle_id");
|
|
38
|
+
assert(data.total_thrown >= 1, "should count thrown bottles");
|
|
39
|
+
bottleId = data.bottle_id;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
await test("can't pick your own bottle", async () => {
|
|
43
|
+
const { data, error } = await sb.rpc("pick_drift_bottle", {
|
|
44
|
+
p_device_id: DEVICE_A,
|
|
45
|
+
});
|
|
46
|
+
assert(!error, `RPC error: ${error?.message}`);
|
|
47
|
+
// Either no bottle found (if A's is the only one) or found someone else's
|
|
48
|
+
if (data.found) {
|
|
49
|
+
assert(data.bottle_id !== bottleId, "should not pick own bottle");
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
await test("pick a bottle as another user", async () => {
|
|
54
|
+
const { data, error } = await sb.rpc("pick_drift_bottle", {
|
|
55
|
+
p_device_id: DEVICE_B,
|
|
56
|
+
});
|
|
57
|
+
assert(!error, `RPC error: ${error?.message}`);
|
|
58
|
+
assert(data.found === true, "should find a bottle");
|
|
59
|
+
assert(data.bottle_id === bottleId, "should be alice's bottle");
|
|
60
|
+
assert(data.message != null, "should have message content");
|
|
61
|
+
// Privacy: no sender device_id in response
|
|
62
|
+
assert(JSON.stringify(data).indexOf(DEVICE_A) === -1, "PRIVACY: should NOT contain sender device_id");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
await test("can't pick another while holding one", async () => {
|
|
66
|
+
const { data, error } = await sb.rpc("pick_drift_bottle", {
|
|
67
|
+
p_device_id: DEVICE_B,
|
|
68
|
+
});
|
|
69
|
+
assert(!error, `RPC error: ${error?.message}`);
|
|
70
|
+
assert(data.error === "you_have_unreplied_bottle", "should block picking another");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
await test("reply to bottle", async () => {
|
|
74
|
+
const { data, error } = await sb.rpc("reply_drift_bottle", {
|
|
75
|
+
p_bottle_id: bottleId,
|
|
76
|
+
p_device_id: DEVICE_B,
|
|
77
|
+
p_reply: "你好!收到你的漂流瓶了 🎉",
|
|
78
|
+
});
|
|
79
|
+
assert(!error, `RPC error: ${error?.message}`);
|
|
80
|
+
assert(data.success === true, "should succeed");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
await test("check bottles - sender sees reply", async () => {
|
|
84
|
+
const { data, error } = await sb.rpc("check_drift_bottles", {
|
|
85
|
+
p_device_id: DEVICE_A,
|
|
86
|
+
});
|
|
87
|
+
assert(!error, `RPC error: ${error?.message}`);
|
|
88
|
+
const replies = data.new_replies;
|
|
89
|
+
assert(Array.isArray(replies) && replies.length > 0, "should have new replies");
|
|
90
|
+
assert(replies[0].reply != null, "should contain reply text");
|
|
91
|
+
// Privacy: no picker device_id
|
|
92
|
+
assert(JSON.stringify(data).indexOf(DEVICE_B) === -1, "PRIVACY: should NOT contain picker device_id");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
await test("my bottles shows status", async () => {
|
|
96
|
+
const { data, error } = await sb.rpc("get_my_bottles", {
|
|
97
|
+
p_device_id: DEVICE_A,
|
|
98
|
+
});
|
|
99
|
+
assert(!error, `RPC error: ${error?.message}`);
|
|
100
|
+
const bottles = data.bottles;
|
|
101
|
+
assert(Array.isArray(bottles) && bottles.length > 0, "should have bottles");
|
|
102
|
+
assert(bottles[0].status === "replied", "should be replied status");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Cleanup test data
|
|
106
|
+
await sb.from("drift_bottles").delete().eq("sender_device_id", DEVICE_A);
|
|
107
|
+
await sb.from("drift_bottles").delete().eq("picked_by_device_id", DEVICE_B);
|
|
108
|
+
|
|
109
|
+
console.log("\n🏁 All drift bottle tests done.");
|
package/skills/antenna/EVENTS.md
DELETED
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: antenna-events
|
|
3
|
-
description: "Event management for Antenna. Use when a user wants to create, join, scan, or manage events. Handles event creation, participant management, check-in, approval workflows, and event messaging."
|
|
4
|
-
metadata: { "openclaw": { "always": false } }
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# Antenna Events
|
|
8
|
-
|
|
9
|
-
Event tools for location-based social discovery. Events let organizers gather people, manage participants, and facilitate connections.
|
|
10
|
-
|
|
11
|
-
**Requires:** The core Antenna skill (antenna_scan, antenna_profile, etc.) must also be available. Events build on top of the core profile and matching system.
|
|
12
|
-
|
|
13
|
-
## Event Tools
|
|
14
|
-
|
|
15
|
-
### `antenna_event_create`
|
|
16
|
-
Create an event. Returns a shareable link (antenna.fyi/events/CODE).
|
|
17
|
-
- `name`: event name (required)
|
|
18
|
-
- `sender_id`, `channel`: from context (required)
|
|
19
|
-
- `chat_id`: REQUIRED for notifications
|
|
20
|
-
- `starts_at`, `ends_at`: ISO time strings (required - no default, must be provided)
|
|
21
|
-
- `lat`, `lng`: optional event location (needed for GPS check-in)
|
|
22
|
-
- `description`: optional event description
|
|
23
|
-
- `og_image`: optional OG image URL for social sharing preview
|
|
24
|
-
- `requires_approval`: boolean, default false. If true, participants need organizer approval.
|
|
25
|
-
- `screening_questions`: string array. Questions for applicants.
|
|
26
|
-
|
|
27
|
-
**When the user mentions "审批" / "approval" / "筛选" / "报名表"**, set `requires_approval: true` and ask what questions they want to screen with.
|
|
28
|
-
|
|
29
|
-
**GPS flow for events:** If the user doesn't provide coordinates, generate a bind link (`antenna_bind`) and ask them to open it at the event location. Once GPS comes in, use those coordinates for the event's `lat`/`lng` — do NOT treat this as the user's personal location. The bind link GPS for event creation goes to the event, not the user's profile. Only use `antenna_checkin` when the user wants to update their own location.
|
|
30
|
-
|
|
31
|
-
### `antenna_event_end`
|
|
32
|
-
End an event. Only the creator can end it.
|
|
33
|
-
- `code`: event code
|
|
34
|
-
- `sender_id`, `channel`: from context
|
|
35
|
-
- `chat_id`: REQUIRED for notifications
|
|
36
|
-
|
|
37
|
-
### `antenna_event_join`
|
|
38
|
-
Join an event by code. Auto-checks in if event has started and you're within 1km.
|
|
39
|
-
- `code`: from the event URL (antenna.fyi/events/CODE)
|
|
40
|
-
- `sender_id`, `channel`: from context
|
|
41
|
-
- `chat_id`: REQUIRED for notifications
|
|
42
|
-
- `lat`, `lng`: optional GPS coordinates (for auto-checkin)
|
|
43
|
-
- **Requires a profile** — users without a profile will be told to create one first.
|
|
44
|
-
|
|
45
|
-
### `antenna_event_scan`
|
|
46
|
-
Scan people in an event. No distance limit — returns all participants.
|
|
47
|
-
- `code`: event code
|
|
48
|
-
- `sender_id`, `channel`: from context
|
|
49
|
-
- `chat_id`: REQUIRED for notifications
|
|
50
|
-
- Returns profiles with `source: "event"` tag
|
|
51
|
-
|
|
52
|
-
### `antenna_event_checkin`
|
|
53
|
-
Check in at an event — marks you as present at the event location. Optionally updates GPS.
|
|
54
|
-
- `code`: event code
|
|
55
|
-
- `sender_id`, `channel`: from context
|
|
56
|
-
- `chat_id`: REQUIRED for notifications
|
|
57
|
-
- `lat`, `lng`: optional GPS coordinates
|
|
58
|
-
- **Event must have started** (`starts_at <= now`). Cannot check in before start time.
|
|
59
|
-
- **Must be within 1km** of event location.
|
|
60
|
-
- **Must have `status: active`** (approved participants only, not pending).
|
|
61
|
-
- **Check-in is automatic on join.** Only call this manually if the user explicitly asks to check in. Do not prompt the user about check-in.
|
|
62
|
-
|
|
63
|
-
### `antenna_event_upload_image`
|
|
64
|
-
Upload an image for an event OG preview. Returns a public URL.
|
|
65
|
-
- `image_base64`: base64-encoded image data
|
|
66
|
-
- `content_type`: MIME type (default image/png)
|
|
67
|
-
- `event_code`: event code
|
|
68
|
-
|
|
69
|
-
### `antenna_event_update`
|
|
70
|
-
Update event info. Only creator or co-host can update.
|
|
71
|
-
- `code`: event code
|
|
72
|
-
- `sender_id`, `channel`: from context
|
|
73
|
-
- `chat_id`: REQUIRED for notifications
|
|
74
|
-
- `name`, `description`, `og_image`, `lat`, `lng`, `starts_at`, `ends_at`: all optional for update (only provided fields change, others stay as-is)
|
|
75
|
-
- `requires_approval`: optional boolean — enable/disable approval requirement
|
|
76
|
-
- `screening_questions`: optional string array — update screening questions
|
|
77
|
-
|
|
78
|
-
### `antenna_event_approve`
|
|
79
|
-
Approve a pending participant. Only creator or co-host.
|
|
80
|
-
- `code`: event code
|
|
81
|
-
- `sender_id`, `channel`: from context
|
|
82
|
-
- `chat_id`: REQUIRED for notifications
|
|
83
|
-
- `ref`: participant ref number from scan
|
|
84
|
-
|
|
85
|
-
### `antenna_event_reject`
|
|
86
|
-
Reject a pending participant. Only creator or co-host.
|
|
87
|
-
- `code`: event code
|
|
88
|
-
- `sender_id`, `channel`: from context
|
|
89
|
-
- `chat_id`: REQUIRED for notifications
|
|
90
|
-
- `ref`: participant ref number from scan
|
|
91
|
-
|
|
92
|
-
### `antenna_event_add_host`
|
|
93
|
-
Add a co-host to the event. Only creator can add.
|
|
94
|
-
- `code`: event code
|
|
95
|
-
- `sender_id`, `channel`: from context
|
|
96
|
-
- `chat_id`: REQUIRED for notifications
|
|
97
|
-
- `ref`: participant ref number to promote to co-host
|
|
98
|
-
|
|
99
|
-
### `antenna_event_message`
|
|
100
|
-
Send a message to event participants. Only creator or co-host can send.
|
|
101
|
-
- `code`: event code
|
|
102
|
-
- `sender_id`, `channel`: from context
|
|
103
|
-
- `chat_id`: REQUIRED for notifications
|
|
104
|
-
- `message`: the message text
|
|
105
|
-
- `ref`: optional — ref number of specific participant. Omit to broadcast to all active participants.
|
|
106
|
-
- Use when the host needs to notify participants about logistics, changes, or requests (e.g. "please share your WeChat in your profile").
|
|
107
|
-
- One-way: participants receive the message but cannot reply through this channel.
|
|
108
|
-
|
|
109
|
-
---
|
|
110
|
-
|
|
111
|
-
## Event Behavior Guide
|
|
112
|
-
|
|
113
|
-
> This section is the single source of truth for event behavior. Tool descriptions above define parameters; this section defines agent behavior.
|
|
114
|
-
|
|
115
|
-
### Creating an event
|
|
116
|
-
Collect info through conversation (ask one by one, don't dump all at once):
|
|
117
|
-
1. **Event name** (required) — "活动叫什么名字?"
|
|
118
|
-
2. **Description** — "简单描述一下这个活动?"
|
|
119
|
-
3. **Time** (required) — "什么时候开始?大概多长?" (convert to `starts_at` / `ends_at` ISO strings. **Must provide both — no defaults.**)
|
|
120
|
-
4. **Location** — "活动在哪里?" If user gives an address, geocode it. If vague, generate a bind link after creation.
|
|
121
|
-
5. **Approval** — "需要审批参与者吗?" If yes:
|
|
122
|
-
6. **Screening questions** — "你想问报名者什么问题?" Collect as a list.
|
|
123
|
-
|
|
124
|
-
Then call `antenna_event_create` with all collected info.
|
|
125
|
-
If no GPS, call `antenna_bind(purpose="event", event_code=CODE)` and send the link.
|
|
126
|
-
Share the event URL with the user.
|
|
127
|
-
|
|
128
|
-
### Joining an event
|
|
129
|
-
1. Extract the code from `antenna.fyi/events/CODE`
|
|
130
|
-
2. Call `antenna_event_join(code)` — this checks everything:
|
|
131
|
-
- If no profile → "Create a profile first"
|
|
132
|
-
- If event requires approval and no `application_context` provided → returns `needs_screening: true` + `screening_questions`
|
|
133
|
-
- If screening questions returned: **ask the user each question**, collect answers, then call `antenna_event_join(code, application_context="answers")` again
|
|
134
|
-
- If `status: pending` → "waiting for organizer approval"
|
|
135
|
-
- If `status: active` → user is in! Auto check-in if event started + GPS within 1km.
|
|
136
|
-
- **Do NOT ask the user about check-in.** Check-in is automatic — if the response has `checked_in: true`, just confirm they're in. If `checked_in: false`, ignore it silently. Users don't need to know about or manage check-in.
|
|
137
|
-
|
|
138
|
-
### Scanning an event
|
|
139
|
-
1. Call `antenna_event_scan(code)`
|
|
140
|
-
2. Hosts see pending participants with `application_context` (screening answers)
|
|
141
|
-
3. Recommend who to meet based on user's interests
|
|
142
|
-
4. Creator/co-host appears with organizer badge
|
|
143
|
-
|
|
144
|
-
### Approving/rejecting participants
|
|
145
|
-
Only creator or co-host can approve/reject:
|
|
146
|
-
- `antenna_event_approve(code, ref)` → participant becomes active
|
|
147
|
-
- `antenna_event_reject(code, ref)` → participant is rejected
|
|
148
|
-
- Notifications are sent automatically to the applicant
|
|
149
|
-
|
|
150
|
-
### Key differences from regular scan
|
|
151
|
-
- `antenna_scan` = nearby discovery, read-only, does NOT write location
|
|
152
|
-
- `antenna_event_scan` = event participants, no distance limit
|
|
153
|
-
- `antenna_checkin` = update YOUR location (not event-related)
|
|
154
|
-
- `antenna_event_checkin` = mark presence at an EVENT (GPS verified, event must have started)
|
|
155
|
-
|
|
156
|
-
### GPS for events
|
|
157
|
-
**Event GPS** — the event's location ("where is the event")
|
|
158
|
-
- Set via `antenna_bind(purpose="event")` or `antenna_event_create(lat, lng)`
|
|
159
|
-
- Precise coordinates (NOT blurred)
|
|
160
|
-
- Used for: check-in distance verification (≤1km), `nearby_events` discovery (5km)
|
|
161
|
-
- Does not expire — event location is fixed
|
|
162
|
-
|
|
163
|
-
**Auto-checkin on join:** When a user joins an event that has already started, the system automatically attempts check-in if GPS is available and within 1km. No user action needed.
|