@memtensor/memos-local-openclaw-plugin 1.0.4-beta.1 → 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 +1 -2
- package/dist/client/connector.d.ts.map +1 -1
- package/dist/client/connector.js +93 -26
- 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 +0 -2
- 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 +277 -87
- package/dist/hub/server.js.map +1 -1
- package/dist/hub/user-manager.d.ts +2 -0
- package/dist/hub/user-manager.d.ts.map +1 -1
- package/dist/hub/user-manager.js +5 -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 +286 -118
- 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 +2660 -889
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +30 -8
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +965 -193
- 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 +91 -28
- package/src/client/hub.ts +18 -0
- package/src/client/skill-sync.ts +14 -0
- package/src/config.ts +0 -2
- package/src/hub/server.ts +259 -78
- package/src/hub/user-manager.ts +7 -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 +295 -144
- 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 +2660 -889
- package/src/viewer/server.ts +888 -177
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,9 +201,14 @@ 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 || "";
|
|
195
208
|
const existingUsers = this.opts.store.listHubUsers();
|
|
196
209
|
const existingUser = existingUsers.find(u => u.username === username);
|
|
197
210
|
if (existingUser) {
|
|
211
|
+
try { this.opts.store.updateHubUserActivity(existingUser.id, joinIp); } catch { /* best-effort */ }
|
|
198
212
|
if (existingUser.status === "active") {
|
|
199
213
|
const token = issueUserToken(
|
|
200
214
|
{ userId: existingUser.id, username: existingUser.username, role: existingUser.role, status: "active" },
|
|
@@ -204,11 +218,22 @@ export class HubServer {
|
|
|
204
218
|
return this.json(res, 200, { status: "active", userId: existingUser.id, userToken: token });
|
|
205
219
|
}
|
|
206
220
|
if (existingUser.status === "pending") {
|
|
221
|
+
this.notifyAdmins("user_join_request", "user", username, "", { dedup: true });
|
|
207
222
|
return this.json(res, 200, { status: "pending", userId: existingUser.id });
|
|
208
223
|
}
|
|
209
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") {
|
|
210
234
|
this.userManager.resetToPending(existingUser.id);
|
|
211
|
-
this.
|
|
235
|
+
this.notifyAdmins("user_join_request", "user", username, "");
|
|
236
|
+
this.opts.log.info(`Hub: removed user "${username}" (${existingUser.id}) re-applied, reset to pending`);
|
|
212
237
|
return this.json(res, 200, { status: "pending", userId: existingUser.id });
|
|
213
238
|
}
|
|
214
239
|
}
|
|
@@ -216,7 +241,9 @@ export class HubServer {
|
|
|
216
241
|
username,
|
|
217
242
|
deviceName: typeof body.deviceName === "string" ? body.deviceName : undefined,
|
|
218
243
|
});
|
|
244
|
+
try { this.opts.store.updateHubUserActivity(user.id, joinIp); } catch { /* best-effort */ }
|
|
219
245
|
this.opts.log.info(`Hub: user "${username}" (${user.id}) registered as pending, awaiting admin approval`);
|
|
246
|
+
this.notifyAdmins("user_join_request", "user", username, "");
|
|
220
247
|
return this.json(res, 200, { status: "pending", userId: user.id });
|
|
221
248
|
}
|
|
222
249
|
|
|
@@ -254,6 +281,20 @@ export class HubServer {
|
|
|
254
281
|
return this.json(res, 429, { error: "rate_limit_exceeded", retryAfterMs: HubServer.RATE_WINDOW_MS });
|
|
255
282
|
}
|
|
256
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
|
+
|
|
257
298
|
if (req.method === "GET" && routePath === "/api/v1/hub/me") {
|
|
258
299
|
const user = this.opts.store.getHubUser(auth.userId);
|
|
259
300
|
if (!user) return this.json(res, 401, { error: "unauthorized" });
|
|
@@ -272,9 +313,11 @@ export class HubServer {
|
|
|
272
313
|
}
|
|
273
314
|
const updated = this.userManager.updateUsername(auth.userId, newUsername);
|
|
274
315
|
if (!updated) return this.json(res, 404, { error: "not_found" });
|
|
316
|
+
const ttlMs = updated.role === "admin" ? 3650 * 24 * 60 * 60 * 1000 : undefined;
|
|
275
317
|
const newToken = issueUserToken(
|
|
276
318
|
{ userId: updated.id, username: newUsername, role: updated.role, status: updated.status },
|
|
277
319
|
this.authSecret,
|
|
320
|
+
ttlMs,
|
|
278
321
|
);
|
|
279
322
|
this.userManager.approveUser(updated.id, newToken);
|
|
280
323
|
this.opts.log.info(`Hub: user "${auth.userId}" renamed to "${newUsername}"`);
|
|
@@ -292,6 +335,7 @@ export class HubServer {
|
|
|
292
335
|
const token = issueUserToken({ userId: String(body.userId), username: String(body.username || ""), role: "member", status: "active" }, this.authSecret);
|
|
293
336
|
const approved = this.userManager.approveUser(String(body.userId), token);
|
|
294
337
|
if (!approved) return this.json(res, 404, { error: "not_found" });
|
|
338
|
+
try { this.opts.store.updateHubUserActivity(String(body.userId), ""); } catch { /* best-effort */ }
|
|
295
339
|
return this.json(res, 200, { status: "active", token });
|
|
296
340
|
}
|
|
297
341
|
|
|
@@ -306,97 +350,85 @@ export class HubServer {
|
|
|
306
350
|
if (req.method === "GET" && routePath === "/api/v1/hub/admin/users") {
|
|
307
351
|
if (auth.role !== "admin") return this.json(res, 403, { error: "forbidden" });
|
|
308
352
|
const users = this.opts.store.listHubUsers().filter(u => u.status === "active");
|
|
309
|
-
|
|
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
|
+
}) });
|
|
310
367
|
}
|
|
311
368
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
if (req.method === "GET" && routePath === "/api/v1/hub/groups") {
|
|
369
|
+
if (req.method === "POST" && routePath === "/api/v1/hub/admin/change-role") {
|
|
315
370
|
if (auth.role !== "admin") return this.json(res, 403, { error: "forbidden" });
|
|
316
|
-
const
|
|
317
|
-
|
|
371
|
+
const body = await this.readJson(req);
|
|
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" });
|
|
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 });
|
|
318
384
|
}
|
|
319
385
|
|
|
320
|
-
if (req.method === "POST" && routePath === "/api/v1/hub/
|
|
386
|
+
if (req.method === "POST" && routePath === "/api/v1/hub/admin/rename-user") {
|
|
321
387
|
if (auth.role !== "admin") return this.json(res, 403, { error: "forbidden" });
|
|
322
388
|
const body = await this.readJson(req);
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
id: groupId,
|
|
328
|
-
name,
|
|
329
|
-
description: String(body.description || ""),
|
|
330
|
-
createdAt: Date.now(),
|
|
331
|
-
});
|
|
332
|
-
return this.json(res, 201, { id: groupId, name });
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
const groupDetailMatch = routePath.match(/^\/api\/v1\/hub\/groups\/([^/]+)$/);
|
|
336
|
-
if (groupDetailMatch) {
|
|
337
|
-
const groupId = decodeURIComponent(groupDetailMatch[1]);
|
|
338
|
-
|
|
339
|
-
if (req.method === "GET") {
|
|
340
|
-
if (auth.role !== "admin") return this.json(res, 403, { error: "forbidden" });
|
|
341
|
-
const group = this.opts.store.getHubGroupById(groupId);
|
|
342
|
-
if (!group) return this.json(res, 404, { error: "not_found" });
|
|
343
|
-
const members = this.opts.store.listHubGroupMembers(groupId);
|
|
344
|
-
return this.json(res, 200, { ...group, members });
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
if (req.method === "PUT") {
|
|
348
|
-
if (auth.role !== "admin") return this.json(res, 403, { error: "forbidden" });
|
|
349
|
-
const existing = this.opts.store.getHubGroupById(groupId);
|
|
350
|
-
if (!existing) return this.json(res, 404, { error: "not_found" });
|
|
351
|
-
const body = await this.readJson(req);
|
|
352
|
-
this.opts.store.upsertHubGroup({
|
|
353
|
-
id: groupId,
|
|
354
|
-
name: String(body.name || existing.name).trim(),
|
|
355
|
-
description: String(body.description ?? existing.description),
|
|
356
|
-
createdAt: existing.createdAt,
|
|
357
|
-
});
|
|
358
|
-
return this.json(res, 200, { ok: true });
|
|
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" });
|
|
359
393
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
if (auth.role !== "admin") return this.json(res, 403, { error: "forbidden" });
|
|
363
|
-
const deleted = this.opts.store.deleteHubGroup(groupId);
|
|
364
|
-
if (!deleted) return this.json(res, 404, { error: "not_found" });
|
|
365
|
-
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" });
|
|
366
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 });
|
|
367
411
|
}
|
|
368
412
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
if (
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
if (!user) return this.json(res, 404, { error: "user_not_found" });
|
|
382
|
-
this.opts.store.addHubGroupMember(groupId, userId);
|
|
383
|
-
return this.json(res, 200, { ok: true });
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
if (req.method === "DELETE") {
|
|
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
|
-
if (!userId) return this.json(res, 400, { error: "userId_required" });
|
|
391
|
-
this.opts.store.removeHubGroupMember(groupId, userId);
|
|
392
|
-
return this.json(res, 200, { ok: true });
|
|
393
|
-
}
|
|
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 });
|
|
394
425
|
}
|
|
395
426
|
|
|
396
427
|
if (req.method === "POST" && routePath === "/api/v1/hub/tasks/share") {
|
|
397
428
|
const body = await this.readJson(req);
|
|
398
429
|
if (!body?.task) return this.json(res, 400, { error: "invalid_payload" });
|
|
399
430
|
const task = { ...body.task, sourceUserId: auth.userId };
|
|
431
|
+
const existingTask = task.sourceTaskId ? this.opts.store.getHubTaskBySource(auth.userId, task.sourceTaskId) : null;
|
|
400
432
|
this.opts.store.upsertHubTask(task);
|
|
401
433
|
const chunks = Array.isArray(body.chunks) ? body.chunks : [];
|
|
402
434
|
const chunkIds: string[] = [];
|
|
@@ -404,16 +436,23 @@ export class HubServer {
|
|
|
404
436
|
this.opts.store.upsertHubChunk({ ...chunk, sourceUserId: auth.userId });
|
|
405
437
|
chunkIds.push(chunk.id);
|
|
406
438
|
}
|
|
407
|
-
// Async embedding: don't block the response
|
|
408
439
|
if (this.opts.embedder && chunkIds.length > 0) {
|
|
409
440
|
this.embedChunksAsync(chunkIds, chunks);
|
|
410
441
|
}
|
|
442
|
+
if (!existingTask) {
|
|
443
|
+
this.notifyAdmins("resource_shared", "task", String(task.title || task.sourceTaskId || ""), auth.userId);
|
|
444
|
+
}
|
|
411
445
|
return this.json(res, 200, { ok: true, chunks: chunkIds.length });
|
|
412
446
|
}
|
|
413
447
|
|
|
414
448
|
if (req.method === "POST" && routePath === "/api/v1/hub/tasks/unshare") {
|
|
415
449
|
const body = await this.readJson(req);
|
|
416
|
-
|
|
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
|
+
}
|
|
417
456
|
return this.json(res, 200, { ok: true });
|
|
418
457
|
}
|
|
419
458
|
|
|
@@ -444,6 +483,9 @@ export class HubServer {
|
|
|
444
483
|
if (this.opts.embedder) {
|
|
445
484
|
this.embedMemoryAsync(memoryId, String(m.summary || ""), String(m.content || ""));
|
|
446
485
|
}
|
|
486
|
+
if (!existing) {
|
|
487
|
+
this.notifyAdmins("resource_shared", "memory", String(m.summary || m.content?.slice(0, 60) || memoryId), auth.userId);
|
|
488
|
+
}
|
|
447
489
|
return this.json(res, 200, { ok: true, memoryId, visibility });
|
|
448
490
|
}
|
|
449
491
|
|
|
@@ -451,7 +493,11 @@ export class HubServer {
|
|
|
451
493
|
const body = await this.readJson(req);
|
|
452
494
|
const sourceChunkId = String(body?.sourceChunkId || "");
|
|
453
495
|
if (!sourceChunkId) return this.json(res, 400, { error: "missing_source_chunk_id" });
|
|
496
|
+
const existing = this.opts.store.getHubMemoryBySource(auth.userId, sourceChunkId);
|
|
454
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
|
+
}
|
|
455
501
|
return this.json(res, 200, { ok: true });
|
|
456
502
|
}
|
|
457
503
|
|
|
@@ -574,6 +620,7 @@ export class HubServer {
|
|
|
574
620
|
visibility: hit.visibility,
|
|
575
621
|
groupName: hit.group_name,
|
|
576
622
|
ownerName: hit.owner_name || "unknown",
|
|
623
|
+
ownerStatus: hit.owner_status || "",
|
|
577
624
|
qualityScore: hit.quality_score,
|
|
578
625
|
}));
|
|
579
626
|
return this.json(res, 200, { hits });
|
|
@@ -601,6 +648,9 @@ export class HubServer {
|
|
|
601
648
|
createdAt: existing?.createdAt ?? Date.now(),
|
|
602
649
|
updatedAt: Date.now(),
|
|
603
650
|
});
|
|
651
|
+
if (!existing) {
|
|
652
|
+
this.notifyAdmins("resource_shared", "skill", String(metadata.name || sourceSkillId), auth.userId);
|
|
653
|
+
}
|
|
604
654
|
return this.json(res, 200, { ok: true, skillId, visibility });
|
|
605
655
|
}
|
|
606
656
|
|
|
@@ -623,7 +673,12 @@ export class HubServer {
|
|
|
623
673
|
|
|
624
674
|
if (req.method === "POST" && routePath === "/api/v1/hub/skills/unpublish") {
|
|
625
675
|
const body = await this.readJson(req);
|
|
626
|
-
|
|
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
|
+
}
|
|
627
682
|
return this.json(res, 200, { ok: true });
|
|
628
683
|
}
|
|
629
684
|
|
|
@@ -635,12 +690,48 @@ export class HubServer {
|
|
|
635
690
|
return this.json(res, 200, { tasks });
|
|
636
691
|
}
|
|
637
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
|
+
|
|
638
725
|
const adminTaskDeleteMatch = req.method === "DELETE" ? routePath.match(/^\/api\/v1\/hub\/admin\/shared-tasks\/([^/]+)$/) : null;
|
|
639
726
|
if (adminTaskDeleteMatch) {
|
|
640
727
|
if (auth.role !== "admin") return this.json(res, 403, { error: "forbidden" });
|
|
641
728
|
const taskId = decodeURIComponent(adminTaskDeleteMatch[1]);
|
|
729
|
+
const taskInfo = this.opts.store.getHubTaskById(taskId);
|
|
642
730
|
const deleted = this.opts.store.deleteHubTaskById(taskId);
|
|
643
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
|
+
}
|
|
644
735
|
return this.json(res, 200, { ok: true });
|
|
645
736
|
}
|
|
646
737
|
|
|
@@ -654,8 +745,12 @@ export class HubServer {
|
|
|
654
745
|
if (adminSkillDeleteMatch) {
|
|
655
746
|
if (auth.role !== "admin") return this.json(res, 403, { error: "forbidden" });
|
|
656
747
|
const skillId = decodeURIComponent(adminSkillDeleteMatch[1]);
|
|
748
|
+
const skillInfo = this.opts.store.getHubSkillById(skillId);
|
|
657
749
|
const deleted = this.opts.store.deleteHubSkillById(skillId);
|
|
658
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
|
+
}
|
|
659
754
|
return this.json(res, 200, { ok: true });
|
|
660
755
|
}
|
|
661
756
|
|
|
@@ -669,8 +764,12 @@ export class HubServer {
|
|
|
669
764
|
if (adminMemoryDeleteMatch) {
|
|
670
765
|
if (auth.role !== "admin") return this.json(res, 403, { error: "forbidden" });
|
|
671
766
|
const memoryId = decodeURIComponent(adminMemoryDeleteMatch[1]);
|
|
767
|
+
const memInfo = this.opts.store.getHubMemoryById(memoryId);
|
|
672
768
|
const deleted = this.opts.store.deleteHubMemoryById(memoryId);
|
|
673
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
|
+
}
|
|
674
773
|
return this.json(res, 200, { ok: true });
|
|
675
774
|
}
|
|
676
775
|
|
|
@@ -693,9 +792,87 @@ export class HubServer {
|
|
|
693
792
|
});
|
|
694
793
|
}
|
|
695
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
|
+
|
|
696
814
|
return this.json(res, 404, { error: "not_found" });
|
|
697
815
|
}
|
|
698
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
|
+
|
|
699
876
|
private authenticate(req: http.IncomingMessage) {
|
|
700
877
|
const header = req.headers.authorization;
|
|
701
878
|
if (!header || !header.startsWith("Bearer ")) return null;
|
|
@@ -706,6 +883,10 @@ export class HubServer {
|
|
|
706
883
|
if (!user || user.status !== "active") return null;
|
|
707
884
|
const hash = createHash("sha256").update(token).digest("hex");
|
|
708
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 */ }
|
|
709
890
|
return {
|
|
710
891
|
userId: user.id,
|
|
711
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 },
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { v4 as uuid } from "uuid";
|
|
2
2
|
import { buildContext } from "./config";
|
|
3
|
+
import { ensureSqliteBinding } from "./storage/ensure-binding";
|
|
3
4
|
import { SqliteStore } from "./storage/sqlite";
|
|
4
5
|
import { Embedder } from "./embedding";
|
|
5
6
|
import { IngestWorker } from "./ingest/worker";
|
|
@@ -56,13 +57,17 @@ export function initPlugin(opts: PluginInitOptions = {}): MemosLocalPlugin {
|
|
|
56
57
|
|
|
57
58
|
ctx.log.info("Initializing memos-local plugin...");
|
|
58
59
|
|
|
60
|
+
ensureSqliteBinding(ctx.log);
|
|
61
|
+
|
|
59
62
|
const store = new SqliteStore(ctx.config.storage!.dbPath!, ctx.log);
|
|
60
63
|
const embedder = new Embedder(ctx.config.embedding, ctx.log, ctx.openclawAPI);
|
|
61
64
|
const worker = new IngestWorker(store, embedder, ctx);
|
|
62
65
|
const engine = new RecallEngine(store, embedder, ctx);
|
|
63
66
|
|
|
67
|
+
const sharedState = { lastSearchTime: 0 };
|
|
68
|
+
|
|
64
69
|
const tools: ToolDefinition[] = [
|
|
65
|
-
createMemorySearchTool(engine, store, ctx),
|
|
70
|
+
createMemorySearchTool(engine, store, ctx, sharedState),
|
|
66
71
|
createMemoryTimelineTool(store),
|
|
67
72
|
createMemoryGetTool(store),
|
|
68
73
|
createNetworkMemoryDetailTool(store, ctx),
|
|
@@ -84,7 +89,10 @@ export function initPlugin(opts: PluginInitOptions = {}): MemosLocalPlugin {
|
|
|
84
89
|
const turnId = uuid();
|
|
85
90
|
const tag = ctx.config.capture?.evidenceWrapperTag ?? "STORED_MEMORY";
|
|
86
91
|
|
|
87
|
-
const
|
|
92
|
+
const userSearchTime = sharedState.lastSearchTime || 0;
|
|
93
|
+
sharedState.lastSearchTime = 0;
|
|
94
|
+
|
|
95
|
+
const captured = captureMessages(messages, session, turnId, tag, ctx.log, owner, userSearchTime);
|
|
88
96
|
if (captured.length > 0) {
|
|
89
97
|
worker.enqueue(captured);
|
|
90
98
|
}
|