@memtensor/memos-local-openclaw-plugin 1.0.4-beta.0 → 1.0.4-beta.10
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/.env.example +7 -0
- package/README.md +24 -24
- package/dist/capture/index.d.ts +1 -1
- package/dist/capture/index.d.ts.map +1 -1
- package/dist/capture/index.js +34 -2
- package/dist/capture/index.js.map +1 -1
- package/dist/client/connector.d.ts +5 -2
- package/dist/client/connector.d.ts.map +1 -1
- package/dist/client/connector.js +173 -14
- package/dist/client/connector.js.map +1 -1
- package/dist/client/hub.d.ts.map +1 -1
- package/dist/client/hub.js +22 -0
- package/dist/client/hub.js.map +1 -1
- package/dist/client/skill-sync.d.ts +7 -0
- package/dist/client/skill-sync.d.ts.map +1 -1
- package/dist/client/skill-sync.js +10 -0
- package/dist/client/skill-sync.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +9 -11
- package/dist/config.js.map +1 -1
- package/dist/hub/server.d.ts +7 -0
- package/dist/hub/server.d.ts.map +1 -1
- package/dist/hub/server.js +301 -106
- package/dist/hub/server.js.map +1 -1
- package/dist/hub/user-manager.d.ts +3 -0
- package/dist/hub/user-manager.d.ts.map +1 -1
- package/dist/hub/user-manager.js +18 -1
- package/dist/hub/user-manager.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -2
- package/dist/index.js.map +1 -1
- package/dist/ingest/providers/index.d.ts.map +1 -1
- package/dist/ingest/providers/index.js +37 -6
- package/dist/ingest/providers/index.js.map +1 -1
- package/dist/recall/engine.d.ts.map +1 -1
- package/dist/recall/engine.js +91 -1
- package/dist/recall/engine.js.map +1 -1
- package/dist/shared/llm-call.d.ts +1 -0
- package/dist/shared/llm-call.d.ts.map +1 -1
- package/dist/shared/llm-call.js +82 -8
- package/dist/shared/llm-call.js.map +1 -1
- package/dist/sharing/types.d.ts +1 -1
- package/dist/sharing/types.d.ts.map +1 -1
- package/dist/skill/evolver.d.ts +2 -0
- package/dist/skill/evolver.d.ts.map +1 -1
- package/dist/skill/evolver.js +3 -0
- package/dist/skill/evolver.js.map +1 -1
- package/dist/storage/ensure-binding.d.ts +12 -0
- package/dist/storage/ensure-binding.d.ts.map +1 -0
- package/dist/storage/ensure-binding.js +53 -0
- package/dist/storage/ensure-binding.js.map +1 -0
- package/dist/storage/sqlite.d.ts +74 -20
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +301 -207
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/telemetry.d.ts +12 -5
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +156 -40
- package/dist/telemetry.js.map +1 -1
- package/dist/tools/memory-search.d.ts +3 -1
- package/dist/tools/memory-search.d.ts.map +1 -1
- package/dist/tools/memory-search.js +3 -1
- package/dist/tools/memory-search.js.map +1 -1
- package/dist/types.d.ts +1 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/viewer/html.d.ts.map +1 -1
- package/dist/viewer/html.js +2991 -1041
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +32 -8
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +1122 -261
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +384 -43
- package/openclaw.plugin.json +1 -1
- package/package.json +3 -2
- package/scripts/postinstall.cjs +1 -1
- package/skill/memos-memory-guide/SKILL.md +64 -26
- package/src/capture/index.ts +37 -1
- package/src/client/connector.ts +173 -16
- package/src/client/hub.ts +18 -0
- package/src/client/skill-sync.ts +14 -0
- package/src/config.ts +9 -11
- package/src/hub/server.ts +285 -98
- package/src/hub/user-manager.ts +20 -3
- package/src/index.ts +10 -2
- package/src/ingest/providers/index.ts +41 -7
- package/src/recall/engine.ts +84 -1
- package/src/shared/llm-call.ts +97 -9
- package/src/sharing/types.ts +1 -1
- package/src/skill/evolver.ts +5 -0
- package/src/storage/ensure-binding.ts +52 -0
- package/src/storage/sqlite.ts +310 -233
- package/src/telemetry.ts +172 -41
- package/src/tools/memory-search.ts +2 -1
- package/src/types.ts +1 -2
- package/src/viewer/html.ts +2991 -1041
- package/src/viewer/server.ts +984 -190
package/src/hub/server.ts
CHANGED
|
@@ -34,6 +34,11 @@ export class HubServer {
|
|
|
34
34
|
private static readonly RATE_LIMIT_SEARCH = 30;
|
|
35
35
|
private rateBuckets = new Map<string, { count: number; windowStart: number }>();
|
|
36
36
|
|
|
37
|
+
private static readonly OFFLINE_THRESHOLD_MS = 2 * 60 * 1000;
|
|
38
|
+
private static readonly OFFLINE_CHECK_INTERVAL_MS = 30 * 1000;
|
|
39
|
+
private offlineCheckTimer?: ReturnType<typeof setInterval>;
|
|
40
|
+
private knownOnlineUsers = new Set<string>();
|
|
41
|
+
|
|
37
42
|
constructor(private opts: HubServerOptions) {
|
|
38
43
|
this.userManager = new HubUserManager(opts.store, opts.log);
|
|
39
44
|
this.authStatePath = path.join(opts.dataDir, "hub-auth.json");
|
|
@@ -101,10 +106,14 @@ export class HubServer {
|
|
|
101
106
|
this.opts.log.info(`memos-local: bootstrap admin token persisted to ${this.authStatePath}`);
|
|
102
107
|
}
|
|
103
108
|
|
|
109
|
+
this.initOnlineTracking();
|
|
110
|
+
this.offlineCheckTimer = setInterval(() => this.checkOfflineUsers(), HubServer.OFFLINE_CHECK_INTERVAL_MS);
|
|
111
|
+
|
|
104
112
|
return `http://127.0.0.1:${this.port}`;
|
|
105
113
|
}
|
|
106
114
|
|
|
107
115
|
async stop(): Promise<void> {
|
|
116
|
+
if (this.offlineCheckTimer) { clearInterval(this.offlineCheckTimer); this.offlineCheckTimer = undefined; }
|
|
108
117
|
if (!this.server) return;
|
|
109
118
|
const server = this.server;
|
|
110
119
|
this.server = undefined;
|
|
@@ -192,17 +201,50 @@ export class HubServer {
|
|
|
192
201
|
return this.json(res, 403, { error: "invalid_team_token" });
|
|
193
202
|
}
|
|
194
203
|
const username = String(body.username || `user-${randomUUID().slice(0, 8)}`);
|
|
204
|
+
const joinIp = (typeof body.clientIp === "string" && body.clientIp)
|
|
205
|
+
|| (req.headers["x-client-ip"] as string)?.trim()
|
|
206
|
+
|| (req.headers["x-forwarded-for"] as string)?.split(",")[0]?.trim()
|
|
207
|
+
|| req.socket.remoteAddress || "";
|
|
208
|
+
const existingUsers = this.opts.store.listHubUsers();
|
|
209
|
+
const existingUser = existingUsers.find(u => u.username === username);
|
|
210
|
+
if (existingUser) {
|
|
211
|
+
try { this.opts.store.updateHubUserActivity(existingUser.id, joinIp); } catch { /* best-effort */ }
|
|
212
|
+
if (existingUser.status === "active") {
|
|
213
|
+
const token = issueUserToken(
|
|
214
|
+
{ userId: existingUser.id, username: existingUser.username, role: existingUser.role, status: "active" },
|
|
215
|
+
this.authSecret,
|
|
216
|
+
);
|
|
217
|
+
this.userManager.approveUser(existingUser.id, token);
|
|
218
|
+
return this.json(res, 200, { status: "active", userId: existingUser.id, userToken: token });
|
|
219
|
+
}
|
|
220
|
+
if (existingUser.status === "pending") {
|
|
221
|
+
this.notifyAdmins("user_join_request", "user", username, "", { dedup: true });
|
|
222
|
+
return this.json(res, 200, { status: "pending", userId: existingUser.id });
|
|
223
|
+
}
|
|
224
|
+
if (existingUser.status === "rejected") {
|
|
225
|
+
if (body.reapply === true) {
|
|
226
|
+
this.userManager.resetToPending(existingUser.id);
|
|
227
|
+
this.notifyAdmins("user_join_request", "user", username, "");
|
|
228
|
+
this.opts.log.info(`Hub: rejected user "${username}" (${existingUser.id}) re-applied, reset to pending`);
|
|
229
|
+
return this.json(res, 200, { status: "pending", userId: existingUser.id });
|
|
230
|
+
}
|
|
231
|
+
return this.json(res, 200, { status: "rejected", userId: existingUser.id });
|
|
232
|
+
}
|
|
233
|
+
if (existingUser.status === "removed") {
|
|
234
|
+
this.userManager.resetToPending(existingUser.id);
|
|
235
|
+
this.notifyAdmins("user_join_request", "user", username, "");
|
|
236
|
+
this.opts.log.info(`Hub: removed user "${username}" (${existingUser.id}) re-applied, reset to pending`);
|
|
237
|
+
return this.json(res, 200, { status: "pending", userId: existingUser.id });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
195
240
|
const user = this.userManager.createPendingUser({
|
|
196
241
|
username,
|
|
197
242
|
deviceName: typeof body.deviceName === "string" ? body.deviceName : undefined,
|
|
198
243
|
});
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
);
|
|
203
|
-
this.userManager.approveUser(user.id, token);
|
|
204
|
-
this.opts.log.info(`Hub: auto-approved user "${username}" (${user.id})`);
|
|
205
|
-
return this.json(res, 200, { status: "active", userId: user.id, userToken: token });
|
|
244
|
+
try { this.opts.store.updateHubUserActivity(user.id, joinIp); } catch { /* best-effort */ }
|
|
245
|
+
this.opts.log.info(`Hub: user "${username}" (${user.id}) registered as pending, awaiting admin approval`);
|
|
246
|
+
this.notifyAdmins("user_join_request", "user", username, "");
|
|
247
|
+
return this.json(res, 200, { status: "pending", userId: user.id });
|
|
206
248
|
}
|
|
207
249
|
|
|
208
250
|
if (req.method === "POST" && routePath === "/api/v1/hub/registration-status") {
|
|
@@ -239,6 +281,20 @@ export class HubServer {
|
|
|
239
281
|
return this.json(res, 429, { error: "rate_limit_exceeded", retryAfterMs: HubServer.RATE_WINDOW_MS });
|
|
240
282
|
}
|
|
241
283
|
|
|
284
|
+
if (req.method === "POST" && routePath === "/api/v1/hub/heartbeat") {
|
|
285
|
+
return this.json(res, 200, { ok: true });
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (req.method === "POST" && routePath === "/api/v1/hub/leave") {
|
|
289
|
+
try {
|
|
290
|
+
this.opts.store.updateHubUserActivity(auth.userId, "", 0);
|
|
291
|
+
} catch { /* best-effort */ }
|
|
292
|
+
this.knownOnlineUsers.delete(auth.userId);
|
|
293
|
+
this.notifyAdmins("user_offline", "user", auth.username, auth.userId);
|
|
294
|
+
this.opts.log.info(`Hub: user "${auth.username}" (${auth.userId}) left voluntarily`);
|
|
295
|
+
return this.json(res, 200, { ok: true });
|
|
296
|
+
}
|
|
297
|
+
|
|
242
298
|
if (req.method === "GET" && routePath === "/api/v1/hub/me") {
|
|
243
299
|
const user = this.opts.store.getHubUser(auth.userId);
|
|
244
300
|
if (!user) return this.json(res, 401, { error: "unauthorized" });
|
|
@@ -257,9 +313,11 @@ export class HubServer {
|
|
|
257
313
|
}
|
|
258
314
|
const updated = this.userManager.updateUsername(auth.userId, newUsername);
|
|
259
315
|
if (!updated) return this.json(res, 404, { error: "not_found" });
|
|
316
|
+
const ttlMs = updated.role === "admin" ? 3650 * 24 * 60 * 60 * 1000 : undefined;
|
|
260
317
|
const newToken = issueUserToken(
|
|
261
318
|
{ userId: updated.id, username: newUsername, role: updated.role, status: updated.status },
|
|
262
319
|
this.authSecret,
|
|
320
|
+
ttlMs,
|
|
263
321
|
);
|
|
264
322
|
this.userManager.approveUser(updated.id, newToken);
|
|
265
323
|
this.opts.log.info(`Hub: user "${auth.userId}" renamed to "${newUsername}"`);
|
|
@@ -277,6 +335,7 @@ export class HubServer {
|
|
|
277
335
|
const token = issueUserToken({ userId: String(body.userId), username: String(body.username || ""), role: "member", status: "active" }, this.authSecret);
|
|
278
336
|
const approved = this.userManager.approveUser(String(body.userId), token);
|
|
279
337
|
if (!approved) return this.json(res, 404, { error: "not_found" });
|
|
338
|
+
try { this.opts.store.updateHubUserActivity(String(body.userId), ""); } catch { /* best-effort */ }
|
|
280
339
|
return this.json(res, 200, { status: "active", token });
|
|
281
340
|
}
|
|
282
341
|
|
|
@@ -291,95 +350,85 @@ export class HubServer {
|
|
|
291
350
|
if (req.method === "GET" && routePath === "/api/v1/hub/admin/users") {
|
|
292
351
|
if (auth.role !== "admin") return this.json(res, 403, { error: "forbidden" });
|
|
293
352
|
const users = this.opts.store.listHubUsers().filter(u => u.status === "active");
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
353
|
+
const contribs = this.opts.store.getHubUserContributions();
|
|
354
|
+
const ownerId = this.authState.bootstrapAdminUserId || "";
|
|
355
|
+
const now = Date.now();
|
|
356
|
+
return this.json(res, 200, { users: users.map(u => {
|
|
357
|
+
const c = contribs[u.id] || { memoryCount: 0, taskCount: 0, skillCount: 0 };
|
|
358
|
+
const isOnline = u.id === ownerId || (!!u.lastActiveAt && now - u.lastActiveAt < HubServer.OFFLINE_THRESHOLD_MS);
|
|
359
|
+
return {
|
|
360
|
+
id: u.id, username: u.username, role: u.role, status: u.status,
|
|
361
|
+
deviceName: u.deviceName, createdAt: u.createdAt, approvedAt: u.approvedAt,
|
|
362
|
+
lastIp: u.lastIp || "", lastActiveAt: u.lastActiveAt,
|
|
363
|
+
isOwner: u.id === ownerId, isOnline,
|
|
364
|
+
memoryCount: c.memoryCount, taskCount: c.taskCount, skillCount: c.skillCount,
|
|
365
|
+
};
|
|
366
|
+
}) });
|
|
302
367
|
}
|
|
303
368
|
|
|
304
|
-
if (req.method === "POST" && routePath === "/api/v1/hub/
|
|
369
|
+
if (req.method === "POST" && routePath === "/api/v1/hub/admin/change-role") {
|
|
305
370
|
if (auth.role !== "admin") return this.json(res, 403, { error: "forbidden" });
|
|
306
371
|
const body = await this.readJson(req);
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
this.
|
|
311
|
-
|
|
312
|
-
name,
|
|
313
|
-
description: String(body.description || ""),
|
|
314
|
-
createdAt: Date.now(),
|
|
315
|
-
});
|
|
316
|
-
return this.json(res, 201, { id: groupId, name });
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const groupDetailMatch = routePath.match(/^\/api\/v1\/hub\/groups\/([^/]+)$/);
|
|
320
|
-
if (groupDetailMatch) {
|
|
321
|
-
const groupId = decodeURIComponent(groupDetailMatch[1]);
|
|
322
|
-
|
|
323
|
-
if (req.method === "GET") {
|
|
324
|
-
const group = this.opts.store.getHubGroupById(groupId);
|
|
325
|
-
if (!group) return this.json(res, 404, { error: "not_found" });
|
|
326
|
-
const members = this.opts.store.listHubGroupMembers(groupId);
|
|
327
|
-
return this.json(res, 200, { ...group, members });
|
|
372
|
+
const userId = String(body?.userId || "");
|
|
373
|
+
const newRole = String(body?.role || "");
|
|
374
|
+
if (!userId || (newRole !== "admin" && newRole !== "member")) return this.json(res, 400, { error: "invalid_params" });
|
|
375
|
+
if (newRole === "member" && userId === this.authState.bootstrapAdminUserId) {
|
|
376
|
+
return this.json(res, 403, { error: "cannot_demote_owner", message: "The hub owner cannot be demoted" });
|
|
328
377
|
}
|
|
378
|
+
const user = this.opts.store.getHubUser(userId);
|
|
379
|
+
if (!user || user.status !== "active") return this.json(res, 404, { error: "not_found" });
|
|
380
|
+
const updatedUser = { ...user, role: newRole as "admin" | "member" };
|
|
381
|
+
this.opts.store.upsertHubUser(updatedUser);
|
|
382
|
+
this.opts.log.info(`Hub: admin "${auth.userId}" changed role of "${userId}" to "${newRole}"`);
|
|
383
|
+
return this.json(res, 200, { ok: true, role: newRole });
|
|
384
|
+
}
|
|
329
385
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
name: String(body.name || existing.name).trim(),
|
|
338
|
-
description: String(body.description ?? existing.description),
|
|
339
|
-
createdAt: existing.createdAt,
|
|
340
|
-
});
|
|
341
|
-
return this.json(res, 200, { ok: true });
|
|
386
|
+
if (req.method === "POST" && routePath === "/api/v1/hub/admin/rename-user") {
|
|
387
|
+
if (auth.role !== "admin") return this.json(res, 403, { error: "forbidden" });
|
|
388
|
+
const body = await this.readJson(req);
|
|
389
|
+
const userId = String(body?.userId || "");
|
|
390
|
+
const newUsername = String(body?.username || "").trim();
|
|
391
|
+
if (!userId || !newUsername || newUsername.length < 2 || newUsername.length > 32) {
|
|
392
|
+
return this.json(res, 400, { error: "invalid_params", message: "userId and username (2-32 chars) required" });
|
|
342
393
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
if (auth.role !== "admin") return this.json(res, 403, { error: "forbidden" });
|
|
346
|
-
const deleted = this.opts.store.deleteHubGroup(groupId);
|
|
347
|
-
if (!deleted) return this.json(res, 404, { error: "not_found" });
|
|
348
|
-
return this.json(res, 200, { ok: true });
|
|
394
|
+
if (this.userManager.isUsernameTaken(newUsername, userId)) {
|
|
395
|
+
return this.json(res, 409, { error: "username_taken", message: "Username already in use" });
|
|
349
396
|
}
|
|
397
|
+
const user = this.opts.store.getHubUser(userId);
|
|
398
|
+
if (!user || user.status !== "active") return this.json(res, 404, { error: "not_found" });
|
|
399
|
+
const ttlMs = user.role === "admin" ? 3650 * 24 * 60 * 60 * 1000 : undefined;
|
|
400
|
+
const newToken = issueUserToken(
|
|
401
|
+
{ userId: user.id, username: newUsername, role: user.role, status: user.status },
|
|
402
|
+
this.authSecret,
|
|
403
|
+
ttlMs,
|
|
404
|
+
);
|
|
405
|
+
this.userManager.approveUser(user.id, newToken);
|
|
406
|
+
const updated = this.opts.store.getHubUser(userId)!;
|
|
407
|
+
const finalUser = { ...updated, username: newUsername };
|
|
408
|
+
this.opts.store.upsertHubUser(finalUser);
|
|
409
|
+
this.opts.log.info(`Hub: admin "${auth.userId}" renamed user "${userId}" to "${newUsername}"`);
|
|
410
|
+
return this.json(res, 200, { ok: true, username: newUsername });
|
|
350
411
|
}
|
|
351
412
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
if (
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
if (!user) return this.json(res, 404, { error: "user_not_found" });
|
|
365
|
-
this.opts.store.addHubGroupMember(groupId, userId);
|
|
366
|
-
return this.json(res, 200, { ok: true });
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
if (req.method === "DELETE") {
|
|
370
|
-
if (auth.role !== "admin") return this.json(res, 403, { error: "forbidden" });
|
|
371
|
-
const body = await this.readJson(req);
|
|
372
|
-
const userId = String(body.userId || "");
|
|
373
|
-
if (!userId) return this.json(res, 400, { error: "userId_required" });
|
|
374
|
-
this.opts.store.removeHubGroupMember(groupId, userId);
|
|
375
|
-
return this.json(res, 200, { ok: true });
|
|
376
|
-
}
|
|
413
|
+
if (req.method === "POST" && routePath === "/api/v1/hub/admin/remove-user") {
|
|
414
|
+
if (auth.role !== "admin") return this.json(res, 403, { error: "forbidden" });
|
|
415
|
+
const body = await this.readJson(req);
|
|
416
|
+
const userId = String(body?.userId || "");
|
|
417
|
+
if (!userId) return this.json(res, 400, { error: "missing_user_id" });
|
|
418
|
+
if (userId === auth.userId) return this.json(res, 400, { error: "cannot_remove_self" });
|
|
419
|
+
if (userId === this.authState.bootstrapAdminUserId) return this.json(res, 403, { error: "cannot_remove_owner", message: "The hub owner cannot be removed" });
|
|
420
|
+
const cleanResources = body?.cleanResources === true;
|
|
421
|
+
const deleted = this.opts.store.deleteHubUser(userId, cleanResources);
|
|
422
|
+
if (!deleted) return this.json(res, 404, { error: "not_found" });
|
|
423
|
+
this.opts.log.info(`Hub: admin "${auth.userId}" removed user "${userId}" (cleanResources=${cleanResources})`);
|
|
424
|
+
return this.json(res, 200, { ok: true });
|
|
377
425
|
}
|
|
378
426
|
|
|
379
427
|
if (req.method === "POST" && routePath === "/api/v1/hub/tasks/share") {
|
|
380
428
|
const body = await this.readJson(req);
|
|
381
429
|
if (!body?.task) return this.json(res, 400, { error: "invalid_payload" });
|
|
382
430
|
const task = { ...body.task, sourceUserId: auth.userId };
|
|
431
|
+
const existingTask = task.sourceTaskId ? this.opts.store.getHubTaskBySource(auth.userId, task.sourceTaskId) : null;
|
|
383
432
|
this.opts.store.upsertHubTask(task);
|
|
384
433
|
const chunks = Array.isArray(body.chunks) ? body.chunks : [];
|
|
385
434
|
const chunkIds: string[] = [];
|
|
@@ -387,16 +436,23 @@ export class HubServer {
|
|
|
387
436
|
this.opts.store.upsertHubChunk({ ...chunk, sourceUserId: auth.userId });
|
|
388
437
|
chunkIds.push(chunk.id);
|
|
389
438
|
}
|
|
390
|
-
// Async embedding: don't block the response
|
|
391
439
|
if (this.opts.embedder && chunkIds.length > 0) {
|
|
392
440
|
this.embedChunksAsync(chunkIds, chunks);
|
|
393
441
|
}
|
|
442
|
+
if (!existingTask) {
|
|
443
|
+
this.notifyAdmins("resource_shared", "task", String(task.title || task.sourceTaskId || ""), auth.userId);
|
|
444
|
+
}
|
|
394
445
|
return this.json(res, 200, { ok: true, chunks: chunkIds.length });
|
|
395
446
|
}
|
|
396
447
|
|
|
397
448
|
if (req.method === "POST" && routePath === "/api/v1/hub/tasks/unshare") {
|
|
398
449
|
const body = await this.readJson(req);
|
|
399
|
-
|
|
450
|
+
const srcTaskId = String(body.sourceTaskId);
|
|
451
|
+
const existing = this.opts.store.getHubTaskBySource(auth.userId, srcTaskId);
|
|
452
|
+
this.opts.store.deleteHubTaskBySource(auth.userId, srcTaskId);
|
|
453
|
+
if (existing) {
|
|
454
|
+
this.notifyAdmins("resource_unshared", "task", existing.title || srcTaskId, auth.userId);
|
|
455
|
+
}
|
|
400
456
|
return this.json(res, 200, { ok: true });
|
|
401
457
|
}
|
|
402
458
|
|
|
@@ -408,15 +464,8 @@ export class HubServer {
|
|
|
408
464
|
if (!sourceChunkId) return this.json(res, 400, { error: "missing_source_chunk_id" });
|
|
409
465
|
const existing = this.opts.store.getHubMemoryBySource(auth.userId, sourceChunkId);
|
|
410
466
|
const memoryId = existing?.id ?? randomUUID();
|
|
411
|
-
const visibility =
|
|
412
|
-
|
|
413
|
-
if (visibility === "group") {
|
|
414
|
-
const gid = String(m.groupId || "");
|
|
415
|
-
if (!gid) return this.json(res, 400, { error: "missing_group_id" });
|
|
416
|
-
const group = this.opts.store.getHubGroupById(gid);
|
|
417
|
-
if (!group) return this.json(res, 404, { error: "group_not_found" });
|
|
418
|
-
resolvedGroupId = gid;
|
|
419
|
-
}
|
|
467
|
+
const visibility = "public";
|
|
468
|
+
const resolvedGroupId: string | null = null;
|
|
420
469
|
const now = Date.now();
|
|
421
470
|
this.opts.store.upsertHubMemory({
|
|
422
471
|
id: memoryId,
|
|
@@ -434,6 +483,9 @@ export class HubServer {
|
|
|
434
483
|
if (this.opts.embedder) {
|
|
435
484
|
this.embedMemoryAsync(memoryId, String(m.summary || ""), String(m.content || ""));
|
|
436
485
|
}
|
|
486
|
+
if (!existing) {
|
|
487
|
+
this.notifyAdmins("resource_shared", "memory", String(m.summary || m.content?.slice(0, 60) || memoryId), auth.userId);
|
|
488
|
+
}
|
|
437
489
|
return this.json(res, 200, { ok: true, memoryId, visibility });
|
|
438
490
|
}
|
|
439
491
|
|
|
@@ -441,7 +493,11 @@ export class HubServer {
|
|
|
441
493
|
const body = await this.readJson(req);
|
|
442
494
|
const sourceChunkId = String(body?.sourceChunkId || "");
|
|
443
495
|
if (!sourceChunkId) return this.json(res, 400, { error: "missing_source_chunk_id" });
|
|
496
|
+
const existing = this.opts.store.getHubMemoryBySource(auth.userId, sourceChunkId);
|
|
444
497
|
this.opts.store.deleteHubMemoryBySource(auth.userId, sourceChunkId);
|
|
498
|
+
if (existing) {
|
|
499
|
+
this.notifyAdmins("resource_unshared", "memory", existing.summary || existing.content?.slice(0, 60) || sourceChunkId, auth.userId);
|
|
500
|
+
}
|
|
445
501
|
return this.json(res, 200, { ok: true });
|
|
446
502
|
}
|
|
447
503
|
|
|
@@ -564,6 +620,7 @@ export class HubServer {
|
|
|
564
620
|
visibility: hit.visibility,
|
|
565
621
|
groupName: hit.group_name,
|
|
566
622
|
ownerName: hit.owner_name || "unknown",
|
|
623
|
+
ownerStatus: hit.owner_status || "",
|
|
567
624
|
qualityScore: hit.quality_score,
|
|
568
625
|
}));
|
|
569
626
|
return this.json(res, 200, { hits });
|
|
@@ -576,7 +633,7 @@ export class HubServer {
|
|
|
576
633
|
if (!sourceSkillId) return this.json(res, 400, { error: "missing_skill_id" });
|
|
577
634
|
const existing = this.opts.store.getHubSkillBySource(auth.userId, sourceSkillId);
|
|
578
635
|
const skillId = existing?.id ?? randomUUID();
|
|
579
|
-
const visibility =
|
|
636
|
+
const visibility = "public";
|
|
580
637
|
this.opts.store.upsertHubSkill({
|
|
581
638
|
id: skillId,
|
|
582
639
|
sourceSkillId,
|
|
@@ -584,13 +641,16 @@ export class HubServer {
|
|
|
584
641
|
name: String(metadata.name || sourceSkillId),
|
|
585
642
|
description: String(metadata.description || ""),
|
|
586
643
|
version: Number(metadata.version || 1),
|
|
587
|
-
groupId:
|
|
644
|
+
groupId: null,
|
|
588
645
|
visibility,
|
|
589
646
|
bundle: JSON.stringify(body?.bundle ?? {}),
|
|
590
647
|
qualityScore: metadata.qualityScore == null ? null : Number(metadata.qualityScore),
|
|
591
648
|
createdAt: existing?.createdAt ?? Date.now(),
|
|
592
649
|
updatedAt: Date.now(),
|
|
593
650
|
});
|
|
651
|
+
if (!existing) {
|
|
652
|
+
this.notifyAdmins("resource_shared", "skill", String(metadata.name || sourceSkillId), auth.userId);
|
|
653
|
+
}
|
|
594
654
|
return this.json(res, 200, { ok: true, skillId, visibility });
|
|
595
655
|
}
|
|
596
656
|
|
|
@@ -598,10 +658,6 @@ export class HubServer {
|
|
|
598
658
|
if (skillBundleMatch) {
|
|
599
659
|
const skill = this.opts.store.getHubSkillById(decodeURIComponent(skillBundleMatch[1]));
|
|
600
660
|
if (!skill) return this.json(res, 404, { error: "not_found" });
|
|
601
|
-
const user = this.opts.store.getHubUser(auth.userId);
|
|
602
|
-
const groups = new Set((user?.groups ?? []).map((group) => group.id));
|
|
603
|
-
const allowed = skill.visibility === "public" || (skill.groupId != null && groups.has(skill.groupId));
|
|
604
|
-
if (!allowed) return this.json(res, 403, { error: "forbidden" });
|
|
605
661
|
return this.json(res, 200, {
|
|
606
662
|
skillId: skill.id,
|
|
607
663
|
metadata: {
|
|
@@ -617,7 +673,12 @@ export class HubServer {
|
|
|
617
673
|
|
|
618
674
|
if (req.method === "POST" && routePath === "/api/v1/hub/skills/unpublish") {
|
|
619
675
|
const body = await this.readJson(req);
|
|
620
|
-
|
|
676
|
+
const srcSkillId = String(body?.sourceSkillId || "");
|
|
677
|
+
const existing = this.opts.store.getHubSkillBySource(auth.userId, srcSkillId);
|
|
678
|
+
this.opts.store.deleteHubSkillBySource(auth.userId, srcSkillId);
|
|
679
|
+
if (existing) {
|
|
680
|
+
this.notifyAdmins("resource_unshared", "skill", existing.name || srcSkillId, auth.userId);
|
|
681
|
+
}
|
|
621
682
|
return this.json(res, 200, { ok: true });
|
|
622
683
|
}
|
|
623
684
|
|
|
@@ -629,12 +690,48 @@ export class HubServer {
|
|
|
629
690
|
return this.json(res, 200, { tasks });
|
|
630
691
|
}
|
|
631
692
|
|
|
693
|
+
const hubTaskDetailMatch = req.method === "GET" ? routePath.match(/^\/api\/v1\/hub\/shared-tasks\/([^/]+)\/detail$/) : null;
|
|
694
|
+
if (hubTaskDetailMatch) {
|
|
695
|
+
const taskId = decodeURIComponent(hubTaskDetailMatch[1]);
|
|
696
|
+
const task = this.opts.store.getHubTaskById(taskId);
|
|
697
|
+
if (!task) return this.json(res, 404, { error: "not_found" });
|
|
698
|
+
const chunks = this.opts.store.listHubChunksByTaskId(taskId);
|
|
699
|
+
return this.json(res, 200, {
|
|
700
|
+
id: task.id, title: task.title, summary: task.summary,
|
|
701
|
+
startedAt: task.createdAt, endedAt: task.updatedAt,
|
|
702
|
+
chunks: chunks.map(c => ({ role: c.role, content: c.content, summary: c.summary, kind: c.kind, createdAt: c.createdAt })),
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const hubSkillDetailMatch = req.method === "GET" ? routePath.match(/^\/api\/v1\/hub\/shared-skills\/([^/]+)\/detail$/) : null;
|
|
707
|
+
if (hubSkillDetailMatch) {
|
|
708
|
+
const skillId = decodeURIComponent(hubSkillDetailMatch[1]);
|
|
709
|
+
const skill = this.opts.store.getHubSkillById(skillId);
|
|
710
|
+
if (!skill) return this.json(res, 404, { error: "not_found" });
|
|
711
|
+
let files: Array<{ path: string; type: string; size: number }> = [];
|
|
712
|
+
try {
|
|
713
|
+
const bundle = JSON.parse(skill.bundle || "{}");
|
|
714
|
+
if (Array.isArray(bundle.files)) {
|
|
715
|
+
files = bundle.files.map((f: any) => ({ path: f.path ?? f.name ?? "unknown", type: f.type ?? "file", size: f.size ?? (f.content ? f.content.length : 0) }));
|
|
716
|
+
}
|
|
717
|
+
} catch { /* ignore parse error */ }
|
|
718
|
+
return this.json(res, 200, {
|
|
719
|
+
skill: { id: skill.id, name: skill.name, description: skill.description, version: skill.version, qualityScore: skill.qualityScore, status: "published" },
|
|
720
|
+
files,
|
|
721
|
+
versions: [],
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
|
|
632
725
|
const adminTaskDeleteMatch = req.method === "DELETE" ? routePath.match(/^\/api\/v1\/hub\/admin\/shared-tasks\/([^/]+)$/) : null;
|
|
633
726
|
if (adminTaskDeleteMatch) {
|
|
634
727
|
if (auth.role !== "admin") return this.json(res, 403, { error: "forbidden" });
|
|
635
728
|
const taskId = decodeURIComponent(adminTaskDeleteMatch[1]);
|
|
729
|
+
const taskInfo = this.opts.store.getHubTaskById(taskId);
|
|
636
730
|
const deleted = this.opts.store.deleteHubTaskById(taskId);
|
|
637
731
|
if (!deleted) return this.json(res, 404, { error: "not_found" });
|
|
732
|
+
if (taskInfo) {
|
|
733
|
+
this.opts.store.insertHubNotification({ id: randomUUID(), userId: taskInfo.sourceUserId, type: "resource_removed", resource: "task", title: taskInfo.title });
|
|
734
|
+
}
|
|
638
735
|
return this.json(res, 200, { ok: true });
|
|
639
736
|
}
|
|
640
737
|
|
|
@@ -648,8 +745,12 @@ export class HubServer {
|
|
|
648
745
|
if (adminSkillDeleteMatch) {
|
|
649
746
|
if (auth.role !== "admin") return this.json(res, 403, { error: "forbidden" });
|
|
650
747
|
const skillId = decodeURIComponent(adminSkillDeleteMatch[1]);
|
|
748
|
+
const skillInfo = this.opts.store.getHubSkillById(skillId);
|
|
651
749
|
const deleted = this.opts.store.deleteHubSkillById(skillId);
|
|
652
750
|
if (!deleted) return this.json(res, 404, { error: "not_found" });
|
|
751
|
+
if (skillInfo) {
|
|
752
|
+
this.opts.store.insertHubNotification({ id: randomUUID(), userId: skillInfo.sourceUserId, type: "resource_removed", resource: "skill", title: skillInfo.name });
|
|
753
|
+
}
|
|
653
754
|
return this.json(res, 200, { ok: true });
|
|
654
755
|
}
|
|
655
756
|
|
|
@@ -663,8 +764,12 @@ export class HubServer {
|
|
|
663
764
|
if (adminMemoryDeleteMatch) {
|
|
664
765
|
if (auth.role !== "admin") return this.json(res, 403, { error: "forbidden" });
|
|
665
766
|
const memoryId = decodeURIComponent(adminMemoryDeleteMatch[1]);
|
|
767
|
+
const memInfo = this.opts.store.getHubMemoryById(memoryId);
|
|
666
768
|
const deleted = this.opts.store.deleteHubMemoryById(memoryId);
|
|
667
769
|
if (!deleted) return this.json(res, 404, { error: "not_found" });
|
|
770
|
+
if (memInfo) {
|
|
771
|
+
this.opts.store.insertHubNotification({ id: randomUUID(), userId: memInfo.sourceUserId, type: "resource_removed", resource: "memory", title: memInfo.summary || memInfo.id });
|
|
772
|
+
}
|
|
668
773
|
return this.json(res, 200, { ok: true });
|
|
669
774
|
}
|
|
670
775
|
|
|
@@ -687,9 +792,87 @@ export class HubServer {
|
|
|
687
792
|
});
|
|
688
793
|
}
|
|
689
794
|
|
|
795
|
+
if (req.method === "GET" && routePath === "/api/v1/hub/notifications") {
|
|
796
|
+
const unread = (new URL(req.url!, `http://${req.headers.host}`)).searchParams.get("unread") === "1";
|
|
797
|
+
const list = this.opts.store.listHubNotifications(auth.userId, { unreadOnly: unread, limit: 50 });
|
|
798
|
+
const unreadCount = this.opts.store.countUnreadHubNotifications(auth.userId);
|
|
799
|
+
return this.json(res, 200, { notifications: list, unreadCount });
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
if (req.method === "POST" && routePath === "/api/v1/hub/notifications/read") {
|
|
803
|
+
const body = await this.readJson(req);
|
|
804
|
+
const ids = Array.isArray(body.ids) ? body.ids as string[] : undefined;
|
|
805
|
+
this.opts.store.markHubNotificationsRead(auth.userId, ids);
|
|
806
|
+
return this.json(res, 200, { ok: true });
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
if (req.method === "POST" && routePath === "/api/v1/hub/notifications/clear") {
|
|
810
|
+
this.opts.store.clearHubNotifications(auth.userId);
|
|
811
|
+
return this.json(res, 200, { ok: true });
|
|
812
|
+
}
|
|
813
|
+
|
|
690
814
|
return this.json(res, 404, { error: "not_found" });
|
|
691
815
|
}
|
|
692
816
|
|
|
817
|
+
private notifyAdmins(type: string, resource: string, title: string, fromUserId: string, opts?: { dedup?: boolean; deduoWindowMs?: number }): void {
|
|
818
|
+
try {
|
|
819
|
+
const admins = this.opts.store.listHubUsers("active").filter(u => u.role === "admin" && u.id !== fromUserId);
|
|
820
|
+
for (const admin of admins) {
|
|
821
|
+
if (opts?.dedup && this.opts.store.hasRecentHubNotification(admin.id, type, resource, opts.deduoWindowMs ?? 300_000)) {
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
this.opts.store.insertHubNotification({ id: randomUUID(), userId: admin.id, type, resource, title });
|
|
825
|
+
}
|
|
826
|
+
} catch { /* best-effort */ }
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
private initOnlineTracking(): void {
|
|
830
|
+
try {
|
|
831
|
+
const ownerId = this.authState.bootstrapAdminUserId || "";
|
|
832
|
+
const users = this.opts.store.listHubUsers("active");
|
|
833
|
+
const now = Date.now();
|
|
834
|
+
for (const u of users) {
|
|
835
|
+
if (u.id === ownerId) continue;
|
|
836
|
+
if (u.lastActiveAt && now - u.lastActiveAt < HubServer.OFFLINE_THRESHOLD_MS) {
|
|
837
|
+
this.knownOnlineUsers.add(u.id);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
} catch { /* best-effort */ }
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
private checkOfflineUsers(): void {
|
|
844
|
+
try {
|
|
845
|
+
const ownerId = this.authState.bootstrapAdminUserId || "";
|
|
846
|
+
const users = this.opts.store.listHubUsers("active");
|
|
847
|
+
const now = Date.now();
|
|
848
|
+
const currentlyOnline = new Set<string>();
|
|
849
|
+
for (const u of users) {
|
|
850
|
+
if (u.id === ownerId) continue;
|
|
851
|
+
if (u.lastActiveAt && now - u.lastActiveAt < HubServer.OFFLINE_THRESHOLD_MS) {
|
|
852
|
+
currentlyOnline.add(u.id);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
for (const uid of this.knownOnlineUsers) {
|
|
856
|
+
if (!currentlyOnline.has(uid)) {
|
|
857
|
+
const user = users.find(u => u.id === uid);
|
|
858
|
+
if (user) {
|
|
859
|
+
this.notifyAdmins("user_offline", "user", user.username, uid);
|
|
860
|
+
this.opts.log.info(`Hub: user "${user.username}" (${uid}) went offline`);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
for (const uid of currentlyOnline) {
|
|
865
|
+
if (!this.knownOnlineUsers.has(uid)) {
|
|
866
|
+
const user = users.find(u => u.id === uid);
|
|
867
|
+
if (user) {
|
|
868
|
+
this.notifyAdmins("user_online", "user", user.username, uid);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
this.knownOnlineUsers = currentlyOnline;
|
|
873
|
+
} catch { /* best-effort */ }
|
|
874
|
+
}
|
|
875
|
+
|
|
693
876
|
private authenticate(req: http.IncomingMessage) {
|
|
694
877
|
const header = req.headers.authorization;
|
|
695
878
|
if (!header || !header.startsWith("Bearer ")) return null;
|
|
@@ -700,6 +883,10 @@ export class HubServer {
|
|
|
700
883
|
if (!user || user.status !== "active") return null;
|
|
701
884
|
const hash = createHash("sha256").update(token).digest("hex");
|
|
702
885
|
if (user.tokenHash !== hash) return null;
|
|
886
|
+
const clientIp = (req.headers["x-client-ip"] as string)?.trim()
|
|
887
|
+
|| (req.headers["x-forwarded-for"] as string)?.split(",")[0]?.trim()
|
|
888
|
+
|| req.socket.remoteAddress || "";
|
|
889
|
+
try { this.opts.store.updateHubUserActivity(user.id, clientIp); } catch { /* best-effort */ }
|
|
703
890
|
return {
|
|
704
891
|
userId: user.id,
|
|
705
892
|
username: user.username,
|
package/src/hub/user-manager.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { randomUUID, createHash } from "crypto";
|
|
2
|
-
import { issueUserToken } from "./auth";
|
|
2
|
+
import { issueUserToken, verifyUserToken } from "./auth";
|
|
3
3
|
import type { Logger } from "../types";
|
|
4
4
|
import type { UserInfo } from "../sharing/types";
|
|
5
5
|
import type { SqliteStore } from "../storage/sqlite";
|
|
6
6
|
|
|
7
|
-
type ManagedHubUser = UserInfo & { tokenHash: string; createdAt: number; approvedAt: number | null };
|
|
7
|
+
type ManagedHubUser = UserInfo & { tokenHash: string; createdAt: number; approvedAt: number | null; lastIp: string; lastActiveAt: number | null };
|
|
8
8
|
|
|
9
9
|
export class HubUserManager {
|
|
10
10
|
constructor(private store: SqliteStore, private log: Logger) {}
|
|
@@ -20,6 +20,8 @@ export class HubUserManager {
|
|
|
20
20
|
tokenHash: "",
|
|
21
21
|
createdAt: Date.now(),
|
|
22
22
|
approvedAt: null,
|
|
23
|
+
lastIp: "",
|
|
24
|
+
lastActiveAt: null,
|
|
23
25
|
};
|
|
24
26
|
this.store.upsertHubUser(user);
|
|
25
27
|
return user;
|
|
@@ -46,7 +48,7 @@ export class HubUserManager {
|
|
|
46
48
|
if (bootstrapUserId) {
|
|
47
49
|
const bootstrapUser = this.store.getHubUser(bootstrapUserId);
|
|
48
50
|
if (bootstrapUser && bootstrapUser.role === "admin" && bootstrapUser.status === "active") {
|
|
49
|
-
if (bootstrapToken && bootstrapUser.tokenHash === createHash("sha256").update(bootstrapToken).digest("hex")) {
|
|
51
|
+
if (bootstrapToken && bootstrapUser.tokenHash === createHash("sha256").update(bootstrapToken).digest("hex") && verifyUserToken(bootstrapToken, secret)) {
|
|
50
52
|
return { user: bootstrapUser, token: bootstrapToken };
|
|
51
53
|
}
|
|
52
54
|
const refreshedToken = issueUserToken(
|
|
@@ -88,6 +90,8 @@ export class HubUserManager {
|
|
|
88
90
|
tokenHash: "",
|
|
89
91
|
createdAt: Date.now(),
|
|
90
92
|
approvedAt: Date.now(),
|
|
93
|
+
lastIp: "",
|
|
94
|
+
lastActiveAt: null,
|
|
91
95
|
};
|
|
92
96
|
const token = issueUserToken(
|
|
93
97
|
{ userId: user.id, username: user.username, role: user.role, status: user.status },
|
|
@@ -123,4 +127,17 @@ export class HubUserManager {
|
|
|
123
127
|
this.store.upsertHubUser(updated);
|
|
124
128
|
return updated;
|
|
125
129
|
}
|
|
130
|
+
|
|
131
|
+
resetToPending(userId: string): ManagedHubUser | null {
|
|
132
|
+
const user = this.store.getHubUser(userId);
|
|
133
|
+
if (!user) return null;
|
|
134
|
+
const updated = {
|
|
135
|
+
...user,
|
|
136
|
+
status: "pending" as const,
|
|
137
|
+
tokenHash: "",
|
|
138
|
+
approvedAt: null,
|
|
139
|
+
};
|
|
140
|
+
this.store.upsertHubUser(updated);
|
|
141
|
+
return updated;
|
|
142
|
+
}
|
|
126
143
|
}
|