antenna-fyi 1.2.1 → 1.2.3
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/bin/antenna.js +9 -0
- package/lib/cli.js +77 -4
- package/lib/core.js +24 -34
- package/lib/hermes-plugin/__init__.py +72 -2
- package/lib/hermes-plugin/schemas.py +86 -0
- package/lib/hermes-plugin/tools.py +172 -1
- package/package.json +1 -1
- package/skill/SKILL.md +14 -0
package/bin/antenna.js
CHANGED
|
@@ -7,7 +7,10 @@ import {
|
|
|
7
7
|
handleAccept,
|
|
8
8
|
handleCheckin,
|
|
9
9
|
handleMatches,
|
|
10
|
+
handleDiscover,
|
|
11
|
+
handleEvent,
|
|
10
12
|
handleBind,
|
|
13
|
+
handlePass,
|
|
11
14
|
handleSetup,
|
|
12
15
|
handleStatus,
|
|
13
16
|
handleInstallSkill,
|
|
@@ -31,8 +34,14 @@ async function main() {
|
|
|
31
34
|
return handleCheckin(f);
|
|
32
35
|
case "matches":
|
|
33
36
|
return handleMatches(f);
|
|
37
|
+
case "discover":
|
|
38
|
+
return handleDiscover(f);
|
|
39
|
+
case "event":
|
|
40
|
+
return handleEvent(f);
|
|
34
41
|
case "bind":
|
|
35
42
|
return handleBind(f);
|
|
43
|
+
case "pass":
|
|
44
|
+
return handlePass(f);
|
|
36
45
|
case "serve": {
|
|
37
46
|
const { startMcpServer } = await import("../lib/mcp.js");
|
|
38
47
|
return startMcpServer();
|
package/lib/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// antenna CLI command handlers
|
|
2
2
|
|
|
3
|
-
import { scan, getProfile, setProfile, accept, checkMatches, checkin, createBindToken } from "./core.js";
|
|
3
|
+
import { scan, getProfile, setProfile, accept, checkMatches, checkin, createBindToken, discover, createEvent, joinEvent, eventScan, pass as passUser } from "./core.js";
|
|
4
4
|
import { createInterface } from "readline";
|
|
5
5
|
import { existsSync, mkdirSync, copyFileSync } from "fs";
|
|
6
6
|
import { join, dirname } from "path";
|
|
@@ -24,10 +24,10 @@ export function parseFlags(args) {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export async function handleScan(f) {
|
|
27
|
-
if (!f.lat
|
|
27
|
+
if (!f.lat && !f.lng && !f.id) return console.error("Usage: antenna scan --lat 39.99 --lng 116.48 [--radius 500] (max 1000) [--id telegram:123]\n Or just: antenna scan --id telegram:123 (uses saved location from GPS bind)");
|
|
28
28
|
const result = await scan({
|
|
29
|
-
lat: +f.lat,
|
|
30
|
-
lng: +f.lng,
|
|
29
|
+
lat: f.lat ? +f.lat : undefined,
|
|
30
|
+
lng: f.lng ? +f.lng : undefined,
|
|
31
31
|
radius_m: +(f.radius || 500),
|
|
32
32
|
device_id: f.id || null,
|
|
33
33
|
});
|
|
@@ -110,6 +110,65 @@ export async function handleMatches(f) {
|
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
export async function handleDiscover(f) {
|
|
114
|
+
if (!f.id) return console.error("Usage: antenna discover --id telegram:123");
|
|
115
|
+
const result = await discover({ device_id: f.id });
|
|
116
|
+
if (result.count === 0) return console.log(result.message || "🌍 No global recommendation available right now.");
|
|
117
|
+
console.log(`🌍 Global discover:\n`);
|
|
118
|
+
result.profiles.forEach((p) => {
|
|
119
|
+
console.log(` ${p.emoji} ${p.name}`);
|
|
120
|
+
if (p.line1) console.log(` ${p.line1}`);
|
|
121
|
+
if (p.line2) console.log(` ${p.line2}`);
|
|
122
|
+
if (p.line3) console.log(` ${p.line3}`);
|
|
123
|
+
if (p.match_reason) console.log(` → ${p.match_reason}`);
|
|
124
|
+
console.log(` ref: ${p.ref}\n`);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function handleEvent(f) {
|
|
129
|
+
const sub = f._?.[0] || Object.keys(f).find(k => ["create", "join", "scan"].includes(k));
|
|
130
|
+
|
|
131
|
+
if (f.create || (!f.join && !f.scan && f.name)) {
|
|
132
|
+
if (!f.name) return console.error("Usage: antenna event --create --name 'AI Meetup'");
|
|
133
|
+
const result = await createEvent({ name: f.name, device_id: f.id || null, lat: f.lat ? +f.lat : undefined, lng: f.lng ? +f.lng : undefined });
|
|
134
|
+
console.log(`\n🎉 Event created!\n`);
|
|
135
|
+
console.log(` Name: ${result.name}`);
|
|
136
|
+
console.log(` Code: ${result.code}`);
|
|
137
|
+
console.log(` URL: ${result.url}`);
|
|
138
|
+
console.log(` Ends: ${result.ends_at}\n`);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (f.join) {
|
|
143
|
+
if (!f.code || !f.id) return console.error("Usage: antenna event --join --code abc123 --id telegram:123");
|
|
144
|
+
const result = await joinEvent({ code: f.code, device_id: f.id });
|
|
145
|
+
if (result.joined) {
|
|
146
|
+
console.log(`\n✅ Joined "${result.name}" (${result.code})\n`);
|
|
147
|
+
} else {
|
|
148
|
+
console.log(`\n❌ ${result.error}\n`);
|
|
149
|
+
}
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (f.scan) {
|
|
154
|
+
if (!f.code) return console.error("Usage: antenna event --scan --code abc123 [--id telegram:123]");
|
|
155
|
+
const result = await eventScan({ code: f.code, device_id: f.id || null });
|
|
156
|
+
if (result.count === 0) return console.log("\n🏟️ No participants yet.\n");
|
|
157
|
+
console.log(`\n🏟️ ${result.count} people in this event:\n`);
|
|
158
|
+
result.profiles.forEach((p) => {
|
|
159
|
+
console.log(` ${p.emoji} ${p.name}`);
|
|
160
|
+
if (p.line1) console.log(` ${p.line1}`);
|
|
161
|
+
console.log(` ref: ${p.ref}\n`);
|
|
162
|
+
});
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
console.log(`Usage:
|
|
167
|
+
antenna event --create --name 'AI Meetup' [--id telegram:123]
|
|
168
|
+
antenna event --join --code abc123 --id telegram:123
|
|
169
|
+
antenna event --scan --code abc123 [--id telegram:123]`);
|
|
170
|
+
}
|
|
171
|
+
|
|
113
172
|
export async function handleBind(f) {
|
|
114
173
|
if (!f.id) return console.error("Usage: antenna bind --id telegram:123");
|
|
115
174
|
const result = await createBindToken({ device_id: f.id });
|
|
@@ -119,6 +178,17 @@ export async function handleBind(f) {
|
|
|
119
178
|
console.log();
|
|
120
179
|
}
|
|
121
180
|
|
|
181
|
+
export async function handlePass(f) {
|
|
182
|
+
if (!f.id) return console.error("Usage: antenna pass --id telegram:123 --target telegram:789");
|
|
183
|
+
if (!f.target && !f.ref) return console.error("Usage: antenna pass --id telegram:123 --target telegram:789 (or --ref 1)");
|
|
184
|
+
const result = await passUser({
|
|
185
|
+
device_id: f.id,
|
|
186
|
+
target_device_id: f.target,
|
|
187
|
+
ref: f.ref,
|
|
188
|
+
});
|
|
189
|
+
console.log("✅ " + (result.message || "Passed."));
|
|
190
|
+
}
|
|
191
|
+
|
|
122
192
|
export async function handleSetup(f) {
|
|
123
193
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
124
194
|
const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
@@ -288,7 +358,10 @@ Usage:
|
|
|
288
358
|
antenna checkin --id telegram:123 --lat 39.99 --lng 116.48
|
|
289
359
|
antenna profile --id telegram:123 [--name Yi --emoji 🦦 --line1 '...']
|
|
290
360
|
antenna accept --id telegram:123 --target telegram:789 [--contact 'WeChat: yi']
|
|
361
|
+
antenna pass --id telegram:123 --target telegram:789 (or --ref 1)
|
|
291
362
|
antenna matches --id telegram:123
|
|
363
|
+
antenna discover --id telegram:123
|
|
364
|
+
antenna event --create --name 'AI Meetup' | --join --code abc123 | --scan --code abc123
|
|
292
365
|
antenna bind --id telegram:123
|
|
293
366
|
antenna serve Start MCP server (stdio transport)
|
|
294
367
|
antenna setup Interactive profile setup [--id telegram:123]
|
package/lib/core.js
CHANGED
|
@@ -10,49 +10,39 @@ const DEFAULT_KEY =
|
|
|
10
10
|
let _client = null;
|
|
11
11
|
let _url = null;
|
|
12
12
|
|
|
13
|
-
// ─── Embedding
|
|
14
|
-
|
|
15
|
-
const GEMINI_EMBEDDING_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-embedding-001:embedContent";
|
|
16
|
-
const GEMINI_FLASH_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent";
|
|
17
|
-
|
|
18
|
-
async function generateMatchReason(myLines, theirLines) {
|
|
19
|
-
const apiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
|
|
20
|
-
if (!apiKey) return null;
|
|
21
|
-
|
|
22
|
-
const prompt = `You are matching two people. Person A: "${myLines}". Person B: "${theirLines}". Write ONE short sentence (under 20 words) in the SAME LANGUAGE as the profiles explaining why they might click. Be specific, not generic. No fluff.`;
|
|
13
|
+
// ─── Embedding & Match Reason (via Supabase Edge Functions) ───────
|
|
23
14
|
|
|
15
|
+
async function generateEmbedding(text) {
|
|
24
16
|
try {
|
|
25
|
-
const
|
|
17
|
+
const sb = getClient();
|
|
18
|
+
const res = await fetch(`${_url || DEFAULT_URL}/functions/v1/generate-embedding`, {
|
|
26
19
|
method: "POST",
|
|
27
|
-
headers: {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}),
|
|
20
|
+
headers: {
|
|
21
|
+
"Content-Type": "application/json",
|
|
22
|
+
"Authorization": `Bearer ${process.env.ANTENNA_SUPABASE_KEY || process.env.ANTENNA_KEY || DEFAULT_KEY}`,
|
|
23
|
+
},
|
|
24
|
+
body: JSON.stringify({ text }),
|
|
32
25
|
});
|
|
33
26
|
if (!res.ok) return null;
|
|
34
27
|
const data = await res.json();
|
|
35
|
-
return data?.
|
|
28
|
+
return data?.embedding || null;
|
|
36
29
|
} catch { return null; }
|
|
37
30
|
}
|
|
38
31
|
|
|
39
|
-
async function
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (!res.ok) return null;
|
|
54
|
-
const data = await res.json();
|
|
55
|
-
return data?.embedding?.values || null;
|
|
32
|
+
async function generateMatchReason(myLines, theirLines) {
|
|
33
|
+
try {
|
|
34
|
+
const res = await fetch(`${_url || DEFAULT_URL}/functions/v1/generate-match-reason`, {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: {
|
|
37
|
+
"Content-Type": "application/json",
|
|
38
|
+
"Authorization": `Bearer ${process.env.ANTENNA_SUPABASE_KEY || process.env.ANTENNA_KEY || DEFAULT_KEY}`,
|
|
39
|
+
},
|
|
40
|
+
body: JSON.stringify({ my_lines: myLines, their_lines: theirLines }),
|
|
41
|
+
});
|
|
42
|
+
if (!res.ok) return null;
|
|
43
|
+
const data = await res.json();
|
|
44
|
+
return data?.reason || null;
|
|
45
|
+
} catch { return null; }
|
|
56
46
|
}
|
|
57
47
|
|
|
58
48
|
export function getClient(url, key) {
|
|
@@ -13,14 +13,27 @@ from .tools import (
|
|
|
13
13
|
handle_checkin,
|
|
14
14
|
handle_check_matches,
|
|
15
15
|
handle_bind,
|
|
16
|
+
handle_pass,
|
|
17
|
+
handle_discover,
|
|
18
|
+
handle_event_create,
|
|
19
|
+
handle_event_join,
|
|
20
|
+
handle_event_scan,
|
|
16
21
|
_sb,
|
|
17
22
|
_device_id,
|
|
23
|
+
_my_device_ids,
|
|
24
|
+
)
|
|
25
|
+
from .schemas import (
|
|
18
26
|
SCAN_SCHEMA,
|
|
19
27
|
PROFILE_SCHEMA,
|
|
20
28
|
ACCEPT_SCHEMA,
|
|
21
29
|
CHECKIN_SCHEMA,
|
|
22
30
|
CHECK_MATCHES_SCHEMA,
|
|
23
31
|
BIND_SCHEMA,
|
|
32
|
+
PASS_SCHEMA,
|
|
33
|
+
DISCOVER_SCHEMA,
|
|
34
|
+
EVENT_CREATE_SCHEMA,
|
|
35
|
+
EVENT_JOIN_SCHEMA,
|
|
36
|
+
EVENT_SCAN_SCHEMA,
|
|
24
37
|
)
|
|
25
38
|
import re
|
|
26
39
|
import time
|
|
@@ -29,6 +42,11 @@ import time
|
|
|
29
42
|
_last_event_check = 0
|
|
30
43
|
_EVENT_CHECK_INTERVAL = 30 # seconds
|
|
31
44
|
|
|
45
|
+
# Track last match check timestamp
|
|
46
|
+
_last_match_check = 0
|
|
47
|
+
_MATCH_CHECK_INTERVAL = 60 # seconds
|
|
48
|
+
_notified_match_keys: set = set() # "deviceA→deviceB" already notified
|
|
49
|
+
|
|
32
50
|
|
|
33
51
|
def register(ctx):
|
|
34
52
|
# ── Tools ─────────────────────────────────────────────────────
|
|
@@ -38,13 +56,65 @@ def register(ctx):
|
|
|
38
56
|
ctx.register_tool("antenna_checkin", CHECKIN_SCHEMA, handle_checkin)
|
|
39
57
|
ctx.register_tool("antenna_check_matches", CHECK_MATCHES_SCHEMA, handle_check_matches)
|
|
40
58
|
ctx.register_tool("antenna_bind", BIND_SCHEMA, handle_bind)
|
|
59
|
+
ctx.register_tool("antenna_pass", PASS_SCHEMA, handle_pass)
|
|
60
|
+
ctx.register_tool("antenna_discover", DISCOVER_SCHEMA, handle_discover)
|
|
61
|
+
ctx.register_tool("antenna_event_create", EVENT_CREATE_SCHEMA, handle_event_create)
|
|
62
|
+
ctx.register_tool("antenna_event_join", EVENT_JOIN_SCHEMA, handle_event_join)
|
|
63
|
+
ctx.register_tool("antenna_event_scan", EVENT_SCAN_SCHEMA, handle_event_scan)
|
|
41
64
|
|
|
42
65
|
# ── Hook: auto-detect location + check web GPS events ─────────
|
|
43
66
|
def on_pre_llm(messages, **kwargs):
|
|
44
|
-
"""Check for location data in messages AND pending web GPS events."""
|
|
45
|
-
global _last_event_check
|
|
67
|
+
"""Check for location data in messages AND pending web GPS events AND new matches."""
|
|
68
|
+
global _last_event_check, _last_match_check
|
|
46
69
|
hints = []
|
|
47
70
|
|
|
71
|
+
now = time.time()
|
|
72
|
+
|
|
73
|
+
# 0. Check for new matches (every 60s)
|
|
74
|
+
if now - _last_match_check > _MATCH_CHECK_INTERVAL and _my_device_ids:
|
|
75
|
+
_last_match_check = now
|
|
76
|
+
try:
|
|
77
|
+
sb = _sb()
|
|
78
|
+
for did in list(_my_device_ids):
|
|
79
|
+
resp = sb.rpc("get_my_matches", {"p_device_id": did}).execute()
|
|
80
|
+
all_matches = resp.data or []
|
|
81
|
+
if not all_matches:
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
incoming = [m for m in all_matches if m.get("device_id_b") == did]
|
|
85
|
+
my = [m for m in all_matches if m.get("device_id_a") == did]
|
|
86
|
+
|
|
87
|
+
for m in incoming:
|
|
88
|
+
key = f"{m['device_id_a']}→{did}"
|
|
89
|
+
if key in _notified_match_keys:
|
|
90
|
+
continue
|
|
91
|
+
_notified_match_keys.add(key)
|
|
92
|
+
|
|
93
|
+
# Get their profile
|
|
94
|
+
prof_resp = sb.rpc("get_profile", {"p_device_id": m["device_id_a"]}).execute()
|
|
95
|
+
p = prof_resp.data or {}
|
|
96
|
+
name = p.get("display_name") or "有人"
|
|
97
|
+
emoji = p.get("emoji") or "👤"
|
|
98
|
+
|
|
99
|
+
# Check if mutual
|
|
100
|
+
i_accepted = any(
|
|
101
|
+
x.get("device_id_b") == m["device_id_a"] for x in my
|
|
102
|
+
)
|
|
103
|
+
if i_accepted:
|
|
104
|
+
contact = m.get("contact_info_a")
|
|
105
|
+
contact_msg = f"对方的联系方式:{contact}" if contact else "对方还没有分享联系方式。"
|
|
106
|
+
hints.append(
|
|
107
|
+
f"[Antenna] 🎉 双向匹配!{emoji} {name} 也接受了你!{contact_msg} "
|
|
108
|
+
f"请立即通知用户这个好消息!"
|
|
109
|
+
)
|
|
110
|
+
else:
|
|
111
|
+
hints.append(
|
|
112
|
+
f"[Antenna] 📩 {emoji} {name} 想认识你!"
|
|
113
|
+
f"请告诉用户有人想认识 TA,并建议用 antenna_check_matches 查看详情。"
|
|
114
|
+
)
|
|
115
|
+
except Exception:
|
|
116
|
+
pass
|
|
117
|
+
|
|
48
118
|
# 1. Check location_events table (web GPS updates)
|
|
49
119
|
now = time.time()
|
|
50
120
|
if now - _last_event_check > _EVENT_CHECK_INTERVAL:
|
|
@@ -134,3 +134,89 @@ BIND_SCHEMA = {
|
|
|
134
134
|
"required": ["sender_id", "channel"],
|
|
135
135
|
},
|
|
136
136
|
}
|
|
137
|
+
|
|
138
|
+
PASS_SCHEMA = {
|
|
139
|
+
"name": "antenna_pass",
|
|
140
|
+
"description": "Pass/skip a person. They won't be recommended again.",
|
|
141
|
+
"parameters": {
|
|
142
|
+
"type": "object",
|
|
143
|
+
"properties": {
|
|
144
|
+
"sender_id": {"type": "string", "description": "The sender's user ID"},
|
|
145
|
+
"channel": {"type": "string", "description": "Platform name"},
|
|
146
|
+
"ref": {
|
|
147
|
+
"type": "string",
|
|
148
|
+
"description": "Ref number from scan/discover results (e.g. '1')",
|
|
149
|
+
},
|
|
150
|
+
"target_device_id": {
|
|
151
|
+
"type": "string",
|
|
152
|
+
"description": "Device ID (use ref instead when possible)",
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
"required": ["sender_id", "channel"],
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
DISCOVER_SCHEMA = {
|
|
160
|
+
"name": "antenna_discover",
|
|
161
|
+
"description": (
|
|
162
|
+
"Get today's global recommendation — the person most similar to you "
|
|
163
|
+
"worldwide. 1 per day, no repeats."
|
|
164
|
+
),
|
|
165
|
+
"parameters": {
|
|
166
|
+
"type": "object",
|
|
167
|
+
"properties": {
|
|
168
|
+
"sender_id": {"type": "string", "description": "The sender's user ID"},
|
|
169
|
+
"channel": {"type": "string", "description": "Platform name"},
|
|
170
|
+
},
|
|
171
|
+
"required": ["sender_id", "channel"],
|
|
172
|
+
},
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
EVENT_CREATE_SCHEMA = {
|
|
176
|
+
"name": "antenna_event_create",
|
|
177
|
+
"description": (
|
|
178
|
+
"Create an event. Returns a shareable link (antenna.fyi/e/CODE) "
|
|
179
|
+
"for participants to join."
|
|
180
|
+
),
|
|
181
|
+
"parameters": {
|
|
182
|
+
"type": "object",
|
|
183
|
+
"properties": {
|
|
184
|
+
"name": {"type": "string", "description": "Event name"},
|
|
185
|
+
"sender_id": {"type": "string", "description": "The sender's user ID"},
|
|
186
|
+
"channel": {"type": "string", "description": "Platform name"},
|
|
187
|
+
"lat": {"type": "number", "description": "Event latitude"},
|
|
188
|
+
"lng": {"type": "number", "description": "Event longitude"},
|
|
189
|
+
"starts_at": {"type": "string", "description": "Start time ISO"},
|
|
190
|
+
"ends_at": {"type": "string", "description": "End time ISO"},
|
|
191
|
+
},
|
|
192
|
+
"required": ["name", "sender_id", "channel"],
|
|
193
|
+
},
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
EVENT_JOIN_SCHEMA = {
|
|
197
|
+
"name": "antenna_event_join",
|
|
198
|
+
"description": "Join an event by its code from the event URL.",
|
|
199
|
+
"parameters": {
|
|
200
|
+
"type": "object",
|
|
201
|
+
"properties": {
|
|
202
|
+
"code": {"type": "string", "description": "Event code"},
|
|
203
|
+
"sender_id": {"type": "string", "description": "The sender's user ID"},
|
|
204
|
+
"channel": {"type": "string", "description": "Platform name"},
|
|
205
|
+
},
|
|
206
|
+
"required": ["code", "sender_id", "channel"],
|
|
207
|
+
},
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
EVENT_SCAN_SCHEMA = {
|
|
211
|
+
"name": "antenna_event_scan",
|
|
212
|
+
"description": "Scan people in an event. No distance limit.",
|
|
213
|
+
"parameters": {
|
|
214
|
+
"type": "object",
|
|
215
|
+
"properties": {
|
|
216
|
+
"code": {"type": "string", "description": "Event code"},
|
|
217
|
+
"sender_id": {"type": "string", "description": "The sender's user ID"},
|
|
218
|
+
"channel": {"type": "string", "description": "Platform name"},
|
|
219
|
+
},
|
|
220
|
+
"required": ["code", "sender_id", "channel"],
|
|
221
|
+
},
|
|
222
|
+
}
|
|
@@ -8,6 +8,7 @@ import json
|
|
|
8
8
|
import math
|
|
9
9
|
import os
|
|
10
10
|
import time
|
|
11
|
+
import urllib.request
|
|
11
12
|
|
|
12
13
|
try:
|
|
13
14
|
from supabase import create_client
|
|
@@ -29,6 +30,7 @@ _client_url = None
|
|
|
29
30
|
_last_scan: dict[str, float] = {}
|
|
30
31
|
SCAN_DEBOUNCE_S = 30
|
|
31
32
|
_last_ref_map: dict[str, str] = {} # ref → device_id from last scan
|
|
33
|
+
_my_device_ids: set[str] = set() # track this user's device_ids for match checking
|
|
32
34
|
|
|
33
35
|
|
|
34
36
|
def _get_url():
|
|
@@ -53,7 +55,9 @@ def _sb():
|
|
|
53
55
|
|
|
54
56
|
|
|
55
57
|
def _device_id(sender_id: str, channel: str) -> str:
|
|
56
|
-
|
|
58
|
+
did = f"{channel}:{sender_id}"
|
|
59
|
+
_my_device_ids.add(did)
|
|
60
|
+
return did
|
|
57
61
|
|
|
58
62
|
|
|
59
63
|
def _fuzzy(lat: float, lng: float) -> tuple[float, float]:
|
|
@@ -274,6 +278,173 @@ def handle_check_matches(params: dict) -> str:
|
|
|
274
278
|
BASE_URL = "https://www.antenna.fyi"
|
|
275
279
|
|
|
276
280
|
|
|
281
|
+
def handle_pass(params: dict) -> str:
|
|
282
|
+
sb = _sb()
|
|
283
|
+
did = _device_id(params["sender_id"], params["channel"])
|
|
284
|
+
|
|
285
|
+
ref = params.get("ref")
|
|
286
|
+
target = params.get("target_device_id")
|
|
287
|
+
if ref and ref in _last_ref_map:
|
|
288
|
+
target = _last_ref_map[ref]
|
|
289
|
+
if not target and ref:
|
|
290
|
+
# Try resolve via RPC
|
|
291
|
+
try:
|
|
292
|
+
resp = sb.rpc("resolve_ref", {"p_device_id": did, "p_ref": ref}).execute()
|
|
293
|
+
if resp.data:
|
|
294
|
+
target = resp.data.get("target_device_id")
|
|
295
|
+
except Exception:
|
|
296
|
+
pass
|
|
297
|
+
if not target:
|
|
298
|
+
return _ok({"error": "No target. Use 'ref' from scan/discover results or 'target_device_id'."})
|
|
299
|
+
|
|
300
|
+
sb.rpc("pass_user", {
|
|
301
|
+
"p_device_id": did,
|
|
302
|
+
"p_target_device_id": target,
|
|
303
|
+
}).execute()
|
|
304
|
+
|
|
305
|
+
return _ok({"passed": True, "message": "已跳过,不会再推荐这个人了。"})
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def handle_discover(params: dict) -> str:
|
|
309
|
+
sb = _sb()
|
|
310
|
+
did = _device_id(params["sender_id"], params["channel"])
|
|
311
|
+
|
|
312
|
+
resp = sb.rpc("global_discover", {"p_device_id": did}).execute()
|
|
313
|
+
results = resp.data or []
|
|
314
|
+
|
|
315
|
+
if not results:
|
|
316
|
+
return _ok({"count": 0, "message": "今天没有新的全球推荐了,明天再来看看。"})
|
|
317
|
+
|
|
318
|
+
global _last_ref_map
|
|
319
|
+
_last_ref_map = {}
|
|
320
|
+
profiles = []
|
|
321
|
+
|
|
322
|
+
# Get my profile for match reason
|
|
323
|
+
my_prof = sb.rpc("get_profile", {"p_device_id": did}).execute()
|
|
324
|
+
my_data = my_prof.data or {}
|
|
325
|
+
my_lines = [my_data.get("line1", ""), my_data.get("line2", ""), my_data.get("line3", "")]
|
|
326
|
+
|
|
327
|
+
for i, p in enumerate(results):
|
|
328
|
+
ref = str(i + 1)
|
|
329
|
+
_last_ref_map[ref] = p.get("device_id")
|
|
330
|
+
|
|
331
|
+
their_lines = [p.get("line1", ""), p.get("line2", ""), p.get("line3", "")]
|
|
332
|
+
|
|
333
|
+
# Generate match reason via Edge Function
|
|
334
|
+
match_reason = None
|
|
335
|
+
try:
|
|
336
|
+
req = urllib.request.Request(
|
|
337
|
+
f"{BUILTIN_URL}/functions/v1/generate-match-reason",
|
|
338
|
+
data=json.dumps({"my_lines": my_lines, "their_lines": their_lines}).encode(),
|
|
339
|
+
headers={"Content-Type": "application/json", "Authorization": f"Bearer {BUILTIN_KEY}"},
|
|
340
|
+
)
|
|
341
|
+
res = urllib.request.urlopen(req, timeout=10)
|
|
342
|
+
body = json.loads(res.read().decode())
|
|
343
|
+
match_reason = body.get("reason")
|
|
344
|
+
except Exception:
|
|
345
|
+
pass
|
|
346
|
+
|
|
347
|
+
profile = {
|
|
348
|
+
"ref": ref,
|
|
349
|
+
"emoji": p.get("emoji") or "\ud83d\udc64",
|
|
350
|
+
"name": p.get("display_name") or "匿名",
|
|
351
|
+
"line1": p.get("line1"),
|
|
352
|
+
"line2": p.get("line2"),
|
|
353
|
+
"line3": p.get("line3"),
|
|
354
|
+
}
|
|
355
|
+
if match_reason:
|
|
356
|
+
profile["match_reason"] = match_reason
|
|
357
|
+
profiles.append(profile)
|
|
358
|
+
|
|
359
|
+
return _ok({
|
|
360
|
+
"count": len(profiles),
|
|
361
|
+
"profiles": profiles,
|
|
362
|
+
"instruction": "这是全球推荐。根据你对用户的了解,判断是否值得推荐,写一句个性化的匹配理由。使用 ref 编号引用。",
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def handle_event_create(params: dict) -> str:
|
|
367
|
+
sb = _sb()
|
|
368
|
+
did = _device_id(params["sender_id"], params["channel"])
|
|
369
|
+
|
|
370
|
+
rpc_params = {
|
|
371
|
+
"p_device_id": did,
|
|
372
|
+
"p_name": params["name"],
|
|
373
|
+
}
|
|
374
|
+
if params.get("lat") is not None:
|
|
375
|
+
rpc_params["p_lat"] = params["lat"]
|
|
376
|
+
if params.get("lng") is not None:
|
|
377
|
+
rpc_params["p_lng"] = params["lng"]
|
|
378
|
+
if params.get("starts_at"):
|
|
379
|
+
rpc_params["p_starts_at"] = params["starts_at"]
|
|
380
|
+
if params.get("ends_at"):
|
|
381
|
+
rpc_params["p_ends_at"] = params["ends_at"]
|
|
382
|
+
|
|
383
|
+
resp = sb.rpc("create_event", rpc_params).execute()
|
|
384
|
+
data = resp.data or {}
|
|
385
|
+
|
|
386
|
+
code = data.get("code", "")
|
|
387
|
+
return _ok({
|
|
388
|
+
"created": True,
|
|
389
|
+
"name": params["name"],
|
|
390
|
+
"code": code,
|
|
391
|
+
"url": f"{BASE_URL}/e/{code}",
|
|
392
|
+
"message": f"活动已创建!分享链接给参加的人:{BASE_URL}/e/{code}",
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def handle_event_join(params: dict) -> str:
|
|
397
|
+
sb = _sb()
|
|
398
|
+
did = _device_id(params["sender_id"], params["channel"])
|
|
399
|
+
|
|
400
|
+
resp = sb.rpc("join_event", {
|
|
401
|
+
"p_device_id": did,
|
|
402
|
+
"p_code": params["code"],
|
|
403
|
+
}).execute()
|
|
404
|
+
data = resp.data or {}
|
|
405
|
+
|
|
406
|
+
if data.get("joined"):
|
|
407
|
+
return _ok({"joined": True, "name": data.get("name", ""), "message": f"已加入活动 \"{data.get('name', '')}\"!"})
|
|
408
|
+
return _ok({"joined": False, "error": data.get("error", "加入失败")})
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def handle_event_scan(params: dict) -> str:
|
|
412
|
+
sb = _sb()
|
|
413
|
+
did = _device_id(params["sender_id"], params["channel"])
|
|
414
|
+
|
|
415
|
+
resp = sb.rpc("event_participants_list", {
|
|
416
|
+
"p_code": params["code"],
|
|
417
|
+
}).execute()
|
|
418
|
+
results = resp.data or []
|
|
419
|
+
|
|
420
|
+
others = [p for p in results if p.get("device_id") != did]
|
|
421
|
+
|
|
422
|
+
if not others:
|
|
423
|
+
return _ok({"count": 0, "profiles": [], "message": "活动里还没有其他人。"})
|
|
424
|
+
|
|
425
|
+
global _last_ref_map
|
|
426
|
+
_last_ref_map = {}
|
|
427
|
+
profiles = []
|
|
428
|
+
for i, p in enumerate(others):
|
|
429
|
+
ref = str(i + 1)
|
|
430
|
+
_last_ref_map[ref] = p.get("device_id")
|
|
431
|
+
profiles.append({
|
|
432
|
+
"ref": ref,
|
|
433
|
+
"emoji": p.get("emoji") or "\ud83d\udc64",
|
|
434
|
+
"name": p.get("display_name") or "匿名",
|
|
435
|
+
"line1": p.get("line1"),
|
|
436
|
+
"line2": p.get("line2"),
|
|
437
|
+
"line3": p.get("line3"),
|
|
438
|
+
"source": "event",
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
return _ok({
|
|
442
|
+
"count": len(profiles),
|
|
443
|
+
"profiles": profiles,
|
|
444
|
+
"instruction": "这些是活动参加者。根据你对用户的了解,推荐值得认识的人。使用 ref 编号引用。",
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
|
|
277
448
|
def handle_bind(params: dict) -> str:
|
|
278
449
|
sb = _sb()
|
|
279
450
|
did = _device_id(params["sender_id"], params["channel"])
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -122,6 +122,20 @@ Get today's global recommendation — the person most similar to you worldwide.
|
|
|
122
122
|
- If all users have been recommended, returns a message saying "wait for new people"
|
|
123
123
|
- Use this in the daily cron job, or when user asks "find someone interesting globally"
|
|
124
124
|
|
|
125
|
+
### `antenna_pass`
|
|
126
|
+
Pass/skip a person. They won't be recommended again.
|
|
127
|
+
- `sender_id`, `channel`: from context
|
|
128
|
+
- `ref`: ref number from scan/discover results (e.g. '1')
|
|
129
|
+
- `target_device_id`: device ID (use ref instead when possible)
|
|
130
|
+
- Use when the user says "skip", "pass", "not interested", etc.
|
|
131
|
+
|
|
132
|
+
### `antenna_checkin`
|
|
133
|
+
Check in at a location — update your position so others can find you when they scan.
|
|
134
|
+
- `lat`, `lng`: coordinates (required)
|
|
135
|
+
- `sender_id`, `channel`: from context
|
|
136
|
+
- `place_name`: optional name of the place
|
|
137
|
+
- Use when the user says "I'm at XX" or wants to be discoverable without scanning others
|
|
138
|
+
|
|
125
139
|
## Data Transparency — what Antenna sends
|
|
126
140
|
|
|
127
141
|
Antenna only communicates with Supabase (bcudjloikmpcqwcptuyd.supabase.co) via HTTPS.
|