antenna-fyi 0.12.2 → 0.14.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/lib/core.js +88 -4
- package/lib/mcp.js +26 -8
- package/package.json +1 -1
- package/skill/SKILL.md +16 -1
package/lib/core.js
CHANGED
|
@@ -109,6 +109,15 @@ export async function scan({ lat, lng, radius_m = 500, device_id, supabaseUrl, s
|
|
|
109
109
|
};
|
|
110
110
|
});
|
|
111
111
|
|
|
112
|
+
// Save refs to DB (persist across agent restarts)
|
|
113
|
+
const saveRefs = async (refMap) => {
|
|
114
|
+
if (device_id && Object.keys(refMap).length > 0) {
|
|
115
|
+
try {
|
|
116
|
+
await sb.rpc("save_scan_refs", { p_owner: device_id, p_refs: JSON.stringify(refMap) });
|
|
117
|
+
} catch { /* best effort */ }
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
112
121
|
// If nobody nearby, fallback to global discover (1 per day)
|
|
113
122
|
if (others.length === 0 && device_id) {
|
|
114
123
|
const { data: globalData } = await sb.rpc("global_discover", {
|
|
@@ -117,10 +126,12 @@ export async function scan({ lat, lng, radius_m = 500, device_id, supabaseUrl, s
|
|
|
117
126
|
});
|
|
118
127
|
const globalOthers = globalData || [];
|
|
119
128
|
if (globalOthers.length > 0) {
|
|
129
|
+
const profs = buildProfiles(globalOthers);
|
|
130
|
+
await saveRefs(_refMap);
|
|
120
131
|
return {
|
|
121
132
|
count: globalOthers.length,
|
|
122
133
|
radius_m,
|
|
123
|
-
profiles:
|
|
134
|
+
profiles: profs,
|
|
124
135
|
_ref_map: _refMap,
|
|
125
136
|
global: true,
|
|
126
137
|
message: `附近 ${radius_m}m 暂时没人。今天的全球推荐——从这 ${globalOthers.length} 个人里挑一个最匹配的推荐给用户。(每天 1 次)`,
|
|
@@ -135,10 +146,13 @@ export async function scan({ lat, lng, radius_m = 500, device_id, supabaseUrl, s
|
|
|
135
146
|
};
|
|
136
147
|
}
|
|
137
148
|
|
|
149
|
+
const profs = buildProfiles(others);
|
|
150
|
+
await saveRefs(_refMap);
|
|
151
|
+
|
|
138
152
|
return {
|
|
139
153
|
count: others.length,
|
|
140
154
|
radius_m,
|
|
141
|
-
profiles:
|
|
155
|
+
profiles: profs,
|
|
142
156
|
_ref_map: _refMap,
|
|
143
157
|
};
|
|
144
158
|
}
|
|
@@ -202,15 +216,26 @@ export async function setProfile({
|
|
|
202
216
|
export async function accept({
|
|
203
217
|
device_id,
|
|
204
218
|
target_device_id,
|
|
219
|
+
ref,
|
|
205
220
|
contact_info,
|
|
206
221
|
supabaseUrl,
|
|
207
222
|
supabaseKey,
|
|
208
223
|
}) {
|
|
209
224
|
const sb = getClient(supabaseUrl, supabaseKey);
|
|
210
225
|
|
|
226
|
+
// Resolve ref from DB if target_device_id not provided
|
|
227
|
+
let targetId = target_device_id;
|
|
228
|
+
if (!targetId && ref && device_id) {
|
|
229
|
+
const { data } = await sb.rpc("resolve_ref", { p_owner: device_id, p_ref: ref });
|
|
230
|
+
targetId = data;
|
|
231
|
+
}
|
|
232
|
+
if (!targetId) {
|
|
233
|
+
return { accepted: false, error: "No target. Ref may have expired — try scanning again." };
|
|
234
|
+
}
|
|
235
|
+
|
|
211
236
|
const { error } = await sb.rpc("upsert_match", {
|
|
212
237
|
p_device_id_a: device_id,
|
|
213
|
-
p_device_id_b:
|
|
238
|
+
p_device_id_b: targetId,
|
|
214
239
|
p_reason: "",
|
|
215
240
|
p_score: 0,
|
|
216
241
|
p_status: "accepted",
|
|
@@ -223,7 +248,7 @@ export async function accept({
|
|
|
223
248
|
const { data: reverse } = await sb
|
|
224
249
|
.from("matches")
|
|
225
250
|
.select("status, contact_info_a")
|
|
226
|
-
.eq("device_id_a",
|
|
251
|
+
.eq("device_id_a", targetId)
|
|
227
252
|
.eq("device_id_b", device_id)
|
|
228
253
|
.eq("status", "accepted")
|
|
229
254
|
.single();
|
|
@@ -337,6 +362,65 @@ export async function checkMatches({ device_id, supabaseUrl, supabaseKey }) {
|
|
|
337
362
|
|
|
338
363
|
// ─── createBindToken ─────────────────────────────────────────────
|
|
339
364
|
|
|
365
|
+
// ─── discover (global recommendation) ─────────────────────────────
|
|
366
|
+
|
|
367
|
+
export async function discover({ device_id, supabaseUrl, supabaseKey }) {
|
|
368
|
+
const sb = getClient(supabaseUrl, supabaseKey);
|
|
369
|
+
|
|
370
|
+
const { data: globalData } = await sb.rpc("global_discover", {
|
|
371
|
+
p_device_id: device_id,
|
|
372
|
+
p_limit: 1,
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
const results = globalData || [];
|
|
376
|
+
if (results.length === 0) {
|
|
377
|
+
// Check if all used up or daily limit
|
|
378
|
+
return {
|
|
379
|
+
count: 0,
|
|
380
|
+
profiles: [],
|
|
381
|
+
message: "今天的全球推荐已用完,或者你已经看过所有人了。等新人加入!",
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Build ref map + persist to DB
|
|
386
|
+
const _refMap = {};
|
|
387
|
+
const profiles = results.map((p, i) => {
|
|
388
|
+
const ref = String(i + 1);
|
|
389
|
+
_refMap[ref] = p.device_id;
|
|
390
|
+
return {
|
|
391
|
+
ref,
|
|
392
|
+
name: p.display_name || "匿名",
|
|
393
|
+
emoji: p.emoji || "👤",
|
|
394
|
+
line1: p.line1,
|
|
395
|
+
line2: p.line2,
|
|
396
|
+
line3: p.line3,
|
|
397
|
+
};
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
// Log who was recommended (for dedup)
|
|
401
|
+
for (const p of results) {
|
|
402
|
+
await sb.rpc("log_recommendation", {
|
|
403
|
+
p_device_id: device_id,
|
|
404
|
+
p_recommended_id: p.device_id,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Persist ref map to DB
|
|
409
|
+
if (device_id && Object.keys(_refMap).length > 0) {
|
|
410
|
+
try {
|
|
411
|
+
await sb.rpc("save_scan_refs", { p_owner: device_id, p_refs: JSON.stringify(_refMap) });
|
|
412
|
+
} catch { /* best effort */ }
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return {
|
|
416
|
+
count: profiles.length,
|
|
417
|
+
profiles,
|
|
418
|
+
_ref_map: _refMap,
|
|
419
|
+
global: true,
|
|
420
|
+
message: `🌍 今天的全球推荐——这个人跟你可能聊得来。`,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
340
424
|
export async function createBindToken({ device_id, supabaseUrl, supabaseKey }) {
|
|
341
425
|
const sb = getClient(supabaseUrl, supabaseKey);
|
|
342
426
|
const { data, error } = await sb.rpc("create_bind_token", { p_device_id: device_id });
|
package/lib/mcp.js
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
checkMatches,
|
|
12
12
|
checkin,
|
|
13
13
|
createBindToken,
|
|
14
|
+
discover,
|
|
14
15
|
deriveDeviceId,
|
|
15
16
|
} from "./core.js";
|
|
16
17
|
|
|
@@ -98,14 +99,7 @@ export async function startMcpServer() {
|
|
|
98
99
|
},
|
|
99
100
|
async ({ sender_id, channel, ref, target_device_id, contact_info }) => {
|
|
100
101
|
try {
|
|
101
|
-
|
|
102
|
-
if (ref && _lastRefMap[ref]) {
|
|
103
|
-
targetId = _lastRefMap[ref];
|
|
104
|
-
}
|
|
105
|
-
if (!targetId) {
|
|
106
|
-
return jsonResult({ error: "No target specified. Use 'ref' from scan results or 'target_device_id'." });
|
|
107
|
-
}
|
|
108
|
-
const result = await accept({ device_id: deriveDeviceId(sender_id, channel), target_device_id: targetId, contact_info });
|
|
102
|
+
const result = await accept({ device_id: deriveDeviceId(sender_id, channel), target_device_id, ref, contact_info });
|
|
109
103
|
return jsonResult(result);
|
|
110
104
|
} catch (e) {
|
|
111
105
|
return jsonResult({ error: e.message });
|
|
@@ -176,6 +170,30 @@ export async function startMcpServer() {
|
|
|
176
170
|
}
|
|
177
171
|
);
|
|
178
172
|
|
|
173
|
+
// ─── antenna_discover ────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
server.tool(
|
|
176
|
+
"antenna_discover",
|
|
177
|
+
"Get today's global recommendation — the person most similar to you worldwide. 1 per day, no repeats.",
|
|
178
|
+
{
|
|
179
|
+
sender_id: z.string().describe("The sender's user ID"),
|
|
180
|
+
channel: z.string().describe("Channel name"),
|
|
181
|
+
},
|
|
182
|
+
async ({ sender_id, channel }) => {
|
|
183
|
+
try {
|
|
184
|
+
const result = await discover({ device_id: deriveDeviceId(sender_id, channel) });
|
|
185
|
+
if (result._ref_map) {
|
|
186
|
+
_lastRefMap = result._ref_map;
|
|
187
|
+
const { _ref_map, ...clean } = result;
|
|
188
|
+
return jsonResult(clean);
|
|
189
|
+
}
|
|
190
|
+
return jsonResult(result);
|
|
191
|
+
} catch (e) {
|
|
192
|
+
return jsonResult({ error: e.message });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
);
|
|
196
|
+
|
|
179
197
|
const transport = new StdioServerTransport();
|
|
180
198
|
await server.connect(transport);
|
|
181
199
|
}
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -43,9 +43,17 @@ onboarding 完成后,帮用户设置一个每小时自动检查匹配的 cron
|
|
|
43
43
|
|
|
44
44
|
示例(Hermes):
|
|
45
45
|
```
|
|
46
|
-
hermes cron add --every 1h --message "Check antenna matches: call antenna_check_matches. If there are new mutual matches or incoming accepts, tell the user. If nothing new, reply HEARTBEAT_OK."
|
|
46
|
+
hermes cron add --every 1h --message "Check antenna matches: call antenna_check_matches. If there are new mutual matches or incoming accepts, tell the user. Also call antenna_discover once per day to get a global recommendation. If nothing new, reply HEARTBEAT_OK."
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
**第五步:问用户要不要全球推荐**
|
|
50
|
+
|
|
51
|
+
cron 设完后,问用户一句:
|
|
52
|
+
> "要我每天帮你找一个全球最像你的人吗?"
|
|
53
|
+
|
|
54
|
+
用户说要 → cron 每天调 `antenna_discover` 推送。
|
|
55
|
+
用户说不要 → 只在 scan 没人时 fallback。
|
|
56
|
+
|
|
49
57
|
## When to use
|
|
50
58
|
|
|
51
59
|
- **首次安装后**: 主动开始 onboarding(名片 → 位置)
|
|
@@ -101,6 +109,13 @@ Generate a GPS binding link. **You MUST call this immediately after saving a pro
|
|
|
101
109
|
- Send this link to the user — they open it on their phone, allow GPS, and their location is automatically shared
|
|
102
110
|
- **MANDATORY after profile save. Do not wait for user to ask.**
|
|
103
111
|
|
|
112
|
+
### `antenna_discover`
|
|
113
|
+
Get today's global recommendation — the person most similar to you worldwide. 1 per day, no repeats.
|
|
114
|
+
- `sender_id`, `channel`: from context
|
|
115
|
+
- Returns 1 profile (embedding similarity match) that hasn't been recommended before
|
|
116
|
+
- If all users have been recommended, returns a message saying "wait for new people"
|
|
117
|
+
- Use this in the daily cron job, or when user asks "find someone interesting globally"
|
|
118
|
+
|
|
104
119
|
## Behavior guidelines
|
|
105
120
|
|
|
106
121
|
### First-time user — 聊天式引导(不要让用户填表)
|