@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/dist/hub/server.js
CHANGED
|
@@ -51,6 +51,10 @@ class HubServer {
|
|
|
51
51
|
static RATE_LIMIT_DEFAULT = 60;
|
|
52
52
|
static RATE_LIMIT_SEARCH = 30;
|
|
53
53
|
rateBuckets = new Map();
|
|
54
|
+
static OFFLINE_THRESHOLD_MS = 2 * 60 * 1000;
|
|
55
|
+
static OFFLINE_CHECK_INTERVAL_MS = 30 * 1000;
|
|
56
|
+
offlineCheckTimer;
|
|
57
|
+
knownOnlineUsers = new Set();
|
|
54
58
|
constructor(opts) {
|
|
55
59
|
this.opts = opts;
|
|
56
60
|
this.userManager = new user_manager_1.HubUserManager(opts.store, opts.log);
|
|
@@ -109,9 +113,15 @@ class HubServer {
|
|
|
109
113
|
this.saveAuthState();
|
|
110
114
|
this.opts.log.info(`memos-local: bootstrap admin token persisted to ${this.authStatePath}`);
|
|
111
115
|
}
|
|
116
|
+
this.initOnlineTracking();
|
|
117
|
+
this.offlineCheckTimer = setInterval(() => this.checkOfflineUsers(), HubServer.OFFLINE_CHECK_INTERVAL_MS);
|
|
112
118
|
return `http://127.0.0.1:${this.port}`;
|
|
113
119
|
}
|
|
114
120
|
async stop() {
|
|
121
|
+
if (this.offlineCheckTimer) {
|
|
122
|
+
clearInterval(this.offlineCheckTimer);
|
|
123
|
+
this.offlineCheckTimer = undefined;
|
|
124
|
+
}
|
|
115
125
|
if (!this.server)
|
|
116
126
|
return;
|
|
117
127
|
const server = this.server;
|
|
@@ -193,20 +203,39 @@ class HubServer {
|
|
|
193
203
|
return this.json(res, 403, { error: "invalid_team_token" });
|
|
194
204
|
}
|
|
195
205
|
const username = String(body.username || `user-${(0, crypto_1.randomUUID)().slice(0, 8)}`);
|
|
206
|
+
const joinIp = (typeof body.clientIp === "string" && body.clientIp)
|
|
207
|
+
|| req.headers["x-client-ip"]?.trim()
|
|
208
|
+
|| req.headers["x-forwarded-for"]?.split(",")[0]?.trim()
|
|
209
|
+
|| req.socket.remoteAddress || "";
|
|
196
210
|
const existingUsers = this.opts.store.listHubUsers();
|
|
197
211
|
const existingUser = existingUsers.find(u => u.username === username);
|
|
198
212
|
if (existingUser) {
|
|
213
|
+
try {
|
|
214
|
+
this.opts.store.updateHubUserActivity(existingUser.id, joinIp);
|
|
215
|
+
}
|
|
216
|
+
catch { /* best-effort */ }
|
|
199
217
|
if (existingUser.status === "active") {
|
|
200
218
|
const token = (0, auth_1.issueUserToken)({ userId: existingUser.id, username: existingUser.username, role: existingUser.role, status: "active" }, this.authSecret);
|
|
201
219
|
this.userManager.approveUser(existingUser.id, token);
|
|
202
220
|
return this.json(res, 200, { status: "active", userId: existingUser.id, userToken: token });
|
|
203
221
|
}
|
|
204
222
|
if (existingUser.status === "pending") {
|
|
223
|
+
this.notifyAdmins("user_join_request", "user", username, "", { dedup: true });
|
|
205
224
|
return this.json(res, 200, { status: "pending", userId: existingUser.id });
|
|
206
225
|
}
|
|
207
226
|
if (existingUser.status === "rejected") {
|
|
227
|
+
if (body.reapply === true) {
|
|
228
|
+
this.userManager.resetToPending(existingUser.id);
|
|
229
|
+
this.notifyAdmins("user_join_request", "user", username, "");
|
|
230
|
+
this.opts.log.info(`Hub: rejected user "${username}" (${existingUser.id}) re-applied, reset to pending`);
|
|
231
|
+
return this.json(res, 200, { status: "pending", userId: existingUser.id });
|
|
232
|
+
}
|
|
233
|
+
return this.json(res, 200, { status: "rejected", userId: existingUser.id });
|
|
234
|
+
}
|
|
235
|
+
if (existingUser.status === "removed") {
|
|
208
236
|
this.userManager.resetToPending(existingUser.id);
|
|
209
|
-
this.
|
|
237
|
+
this.notifyAdmins("user_join_request", "user", username, "");
|
|
238
|
+
this.opts.log.info(`Hub: removed user "${username}" (${existingUser.id}) re-applied, reset to pending`);
|
|
210
239
|
return this.json(res, 200, { status: "pending", userId: existingUser.id });
|
|
211
240
|
}
|
|
212
241
|
}
|
|
@@ -214,7 +243,12 @@ class HubServer {
|
|
|
214
243
|
username,
|
|
215
244
|
deviceName: typeof body.deviceName === "string" ? body.deviceName : undefined,
|
|
216
245
|
});
|
|
246
|
+
try {
|
|
247
|
+
this.opts.store.updateHubUserActivity(user.id, joinIp);
|
|
248
|
+
}
|
|
249
|
+
catch { /* best-effort */ }
|
|
217
250
|
this.opts.log.info(`Hub: user "${username}" (${user.id}) registered as pending, awaiting admin approval`);
|
|
251
|
+
this.notifyAdmins("user_join_request", "user", username, "");
|
|
218
252
|
return this.json(res, 200, { status: "pending", userId: user.id });
|
|
219
253
|
}
|
|
220
254
|
if (req.method === "POST" && routePath === "/api/v1/hub/registration-status") {
|
|
@@ -248,6 +282,19 @@ class HubServer {
|
|
|
248
282
|
if (!this.checkRateLimit(auth.userId, endpointKey)) {
|
|
249
283
|
return this.json(res, 429, { error: "rate_limit_exceeded", retryAfterMs: HubServer.RATE_WINDOW_MS });
|
|
250
284
|
}
|
|
285
|
+
if (req.method === "POST" && routePath === "/api/v1/hub/heartbeat") {
|
|
286
|
+
return this.json(res, 200, { ok: true });
|
|
287
|
+
}
|
|
288
|
+
if (req.method === "POST" && routePath === "/api/v1/hub/leave") {
|
|
289
|
+
try {
|
|
290
|
+
this.opts.store.updateHubUserActivity(auth.userId, "", 0);
|
|
291
|
+
}
|
|
292
|
+
catch { /* best-effort */ }
|
|
293
|
+
this.knownOnlineUsers.delete(auth.userId);
|
|
294
|
+
this.notifyAdmins("user_offline", "user", auth.username, auth.userId);
|
|
295
|
+
this.opts.log.info(`Hub: user "${auth.username}" (${auth.userId}) left voluntarily`);
|
|
296
|
+
return this.json(res, 200, { ok: true });
|
|
297
|
+
}
|
|
251
298
|
if (req.method === "GET" && routePath === "/api/v1/hub/me") {
|
|
252
299
|
const user = this.opts.store.getHubUser(auth.userId);
|
|
253
300
|
if (!user)
|
|
@@ -268,7 +315,8 @@ class HubServer {
|
|
|
268
315
|
const updated = this.userManager.updateUsername(auth.userId, newUsername);
|
|
269
316
|
if (!updated)
|
|
270
317
|
return this.json(res, 404, { error: "not_found" });
|
|
271
|
-
const
|
|
318
|
+
const ttlMs = updated.role === "admin" ? 3650 * 24 * 60 * 60 * 1000 : undefined;
|
|
319
|
+
const newToken = (0, auth_1.issueUserToken)({ userId: updated.id, username: newUsername, role: updated.role, status: updated.status }, this.authSecret, ttlMs);
|
|
272
320
|
this.userManager.approveUser(updated.id, newToken);
|
|
273
321
|
this.opts.log.info(`Hub: user "${auth.userId}" renamed to "${newUsername}"`);
|
|
274
322
|
return this.json(res, 200, { ok: true, username: newUsername, userToken: newToken });
|
|
@@ -286,6 +334,10 @@ class HubServer {
|
|
|
286
334
|
const approved = this.userManager.approveUser(String(body.userId), token);
|
|
287
335
|
if (!approved)
|
|
288
336
|
return this.json(res, 404, { error: "not_found" });
|
|
337
|
+
try {
|
|
338
|
+
this.opts.store.updateHubUserActivity(String(body.userId), "");
|
|
339
|
+
}
|
|
340
|
+
catch { /* best-effort */ }
|
|
289
341
|
return this.json(res, 200, { status: "active", token });
|
|
290
342
|
}
|
|
291
343
|
if (req.method === "POST" && routePath === "/api/v1/hub/admin/reject-user") {
|
|
@@ -301,102 +353,88 @@ class HubServer {
|
|
|
301
353
|
if (auth.role !== "admin")
|
|
302
354
|
return this.json(res, 403, { error: "forbidden" });
|
|
303
355
|
const users = this.opts.store.listHubUsers().filter(u => u.status === "active");
|
|
304
|
-
|
|
356
|
+
const contribs = this.opts.store.getHubUserContributions();
|
|
357
|
+
const ownerId = this.authState.bootstrapAdminUserId || "";
|
|
358
|
+
const now = Date.now();
|
|
359
|
+
return this.json(res, 200, { users: users.map(u => {
|
|
360
|
+
const c = contribs[u.id] || { memoryCount: 0, taskCount: 0, skillCount: 0 };
|
|
361
|
+
const isOnline = u.id === ownerId || (!!u.lastActiveAt && now - u.lastActiveAt < HubServer.OFFLINE_THRESHOLD_MS);
|
|
362
|
+
return {
|
|
363
|
+
id: u.id, username: u.username, role: u.role, status: u.status,
|
|
364
|
+
deviceName: u.deviceName, createdAt: u.createdAt, approvedAt: u.approvedAt,
|
|
365
|
+
lastIp: u.lastIp || "", lastActiveAt: u.lastActiveAt,
|
|
366
|
+
isOwner: u.id === ownerId, isOnline,
|
|
367
|
+
memoryCount: c.memoryCount, taskCount: c.taskCount, skillCount: c.skillCount,
|
|
368
|
+
};
|
|
369
|
+
}) });
|
|
305
370
|
}
|
|
306
|
-
|
|
307
|
-
if (req.method === "GET" && routePath === "/api/v1/hub/groups") {
|
|
371
|
+
if (req.method === "POST" && routePath === "/api/v1/hub/admin/change-role") {
|
|
308
372
|
if (auth.role !== "admin")
|
|
309
373
|
return this.json(res, 403, { error: "forbidden" });
|
|
310
|
-
const
|
|
311
|
-
|
|
374
|
+
const body = await this.readJson(req);
|
|
375
|
+
const userId = String(body?.userId || "");
|
|
376
|
+
const newRole = String(body?.role || "");
|
|
377
|
+
if (!userId || (newRole !== "admin" && newRole !== "member"))
|
|
378
|
+
return this.json(res, 400, { error: "invalid_params" });
|
|
379
|
+
if (newRole === "member" && userId === this.authState.bootstrapAdminUserId) {
|
|
380
|
+
return this.json(res, 403, { error: "cannot_demote_owner", message: "The hub owner cannot be demoted" });
|
|
381
|
+
}
|
|
382
|
+
const user = this.opts.store.getHubUser(userId);
|
|
383
|
+
if (!user || user.status !== "active")
|
|
384
|
+
return this.json(res, 404, { error: "not_found" });
|
|
385
|
+
const updatedUser = { ...user, role: newRole };
|
|
386
|
+
this.opts.store.upsertHubUser(updatedUser);
|
|
387
|
+
this.opts.log.info(`Hub: admin "${auth.userId}" changed role of "${userId}" to "${newRole}"`);
|
|
388
|
+
return this.json(res, 200, { ok: true, role: newRole });
|
|
312
389
|
}
|
|
313
|
-
if (req.method === "POST" && routePath === "/api/v1/hub/
|
|
390
|
+
if (req.method === "POST" && routePath === "/api/v1/hub/admin/rename-user") {
|
|
314
391
|
if (auth.role !== "admin")
|
|
315
392
|
return this.json(res, 403, { error: "forbidden" });
|
|
316
393
|
const body = await this.readJson(req);
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
this.opts.store.upsertHubGroup({
|
|
322
|
-
id: groupId,
|
|
323
|
-
name,
|
|
324
|
-
description: String(body.description || ""),
|
|
325
|
-
createdAt: Date.now(),
|
|
326
|
-
});
|
|
327
|
-
return this.json(res, 201, { id: groupId, name });
|
|
328
|
-
}
|
|
329
|
-
const groupDetailMatch = routePath.match(/^\/api\/v1\/hub\/groups\/([^/]+)$/);
|
|
330
|
-
if (groupDetailMatch) {
|
|
331
|
-
const groupId = decodeURIComponent(groupDetailMatch[1]);
|
|
332
|
-
if (req.method === "GET") {
|
|
333
|
-
if (auth.role !== "admin")
|
|
334
|
-
return this.json(res, 403, { error: "forbidden" });
|
|
335
|
-
const group = this.opts.store.getHubGroupById(groupId);
|
|
336
|
-
if (!group)
|
|
337
|
-
return this.json(res, 404, { error: "not_found" });
|
|
338
|
-
const members = this.opts.store.listHubGroupMembers(groupId);
|
|
339
|
-
return this.json(res, 200, { ...group, members });
|
|
340
|
-
}
|
|
341
|
-
if (req.method === "PUT") {
|
|
342
|
-
if (auth.role !== "admin")
|
|
343
|
-
return this.json(res, 403, { error: "forbidden" });
|
|
344
|
-
const existing = this.opts.store.getHubGroupById(groupId);
|
|
345
|
-
if (!existing)
|
|
346
|
-
return this.json(res, 404, { error: "not_found" });
|
|
347
|
-
const body = await this.readJson(req);
|
|
348
|
-
this.opts.store.upsertHubGroup({
|
|
349
|
-
id: groupId,
|
|
350
|
-
name: String(body.name || existing.name).trim(),
|
|
351
|
-
description: String(body.description ?? existing.description),
|
|
352
|
-
createdAt: existing.createdAt,
|
|
353
|
-
});
|
|
354
|
-
return this.json(res, 200, { ok: true });
|
|
355
|
-
}
|
|
356
|
-
if (req.method === "DELETE") {
|
|
357
|
-
if (auth.role !== "admin")
|
|
358
|
-
return this.json(res, 403, { error: "forbidden" });
|
|
359
|
-
const deleted = this.opts.store.deleteHubGroup(groupId);
|
|
360
|
-
if (!deleted)
|
|
361
|
-
return this.json(res, 404, { error: "not_found" });
|
|
362
|
-
return this.json(res, 200, { ok: true });
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
const groupMembersMatch = routePath.match(/^\/api\/v1\/hub\/groups\/([^/]+)\/members$/);
|
|
366
|
-
if (groupMembersMatch) {
|
|
367
|
-
const groupId = decodeURIComponent(groupMembersMatch[1]);
|
|
368
|
-
if (req.method === "POST") {
|
|
369
|
-
if (auth.role !== "admin")
|
|
370
|
-
return this.json(res, 403, { error: "forbidden" });
|
|
371
|
-
const group = this.opts.store.getHubGroupById(groupId);
|
|
372
|
-
if (!group)
|
|
373
|
-
return this.json(res, 404, { error: "group_not_found" });
|
|
374
|
-
const body = await this.readJson(req);
|
|
375
|
-
const userId = String(body.userId || "");
|
|
376
|
-
if (!userId)
|
|
377
|
-
return this.json(res, 400, { error: "userId_required" });
|
|
378
|
-
const user = this.opts.store.getHubUser(userId);
|
|
379
|
-
if (!user)
|
|
380
|
-
return this.json(res, 404, { error: "user_not_found" });
|
|
381
|
-
this.opts.store.addHubGroupMember(groupId, userId);
|
|
382
|
-
return this.json(res, 200, { ok: true });
|
|
383
|
-
}
|
|
384
|
-
if (req.method === "DELETE") {
|
|
385
|
-
if (auth.role !== "admin")
|
|
386
|
-
return this.json(res, 403, { error: "forbidden" });
|
|
387
|
-
const body = await this.readJson(req);
|
|
388
|
-
const userId = String(body.userId || "");
|
|
389
|
-
if (!userId)
|
|
390
|
-
return this.json(res, 400, { error: "userId_required" });
|
|
391
|
-
this.opts.store.removeHubGroupMember(groupId, userId);
|
|
392
|
-
return this.json(res, 200, { ok: true });
|
|
394
|
+
const userId = String(body?.userId || "");
|
|
395
|
+
const newUsername = String(body?.username || "").trim();
|
|
396
|
+
if (!userId || !newUsername || newUsername.length < 2 || newUsername.length > 32) {
|
|
397
|
+
return this.json(res, 400, { error: "invalid_params", message: "userId and username (2-32 chars) required" });
|
|
393
398
|
}
|
|
399
|
+
if (this.userManager.isUsernameTaken(newUsername, userId)) {
|
|
400
|
+
return this.json(res, 409, { error: "username_taken", message: "Username already in use" });
|
|
401
|
+
}
|
|
402
|
+
const user = this.opts.store.getHubUser(userId);
|
|
403
|
+
if (!user || user.status !== "active")
|
|
404
|
+
return this.json(res, 404, { error: "not_found" });
|
|
405
|
+
const ttlMs = user.role === "admin" ? 3650 * 24 * 60 * 60 * 1000 : undefined;
|
|
406
|
+
const newToken = (0, auth_1.issueUserToken)({ userId: user.id, username: newUsername, role: user.role, status: user.status }, this.authSecret, ttlMs);
|
|
407
|
+
this.userManager.approveUser(user.id, newToken);
|
|
408
|
+
const updated = this.opts.store.getHubUser(userId);
|
|
409
|
+
const finalUser = { ...updated, username: newUsername };
|
|
410
|
+
this.opts.store.upsertHubUser(finalUser);
|
|
411
|
+
this.opts.log.info(`Hub: admin "${auth.userId}" renamed user "${userId}" to "${newUsername}"`);
|
|
412
|
+
return this.json(res, 200, { ok: true, username: newUsername });
|
|
413
|
+
}
|
|
414
|
+
if (req.method === "POST" && routePath === "/api/v1/hub/admin/remove-user") {
|
|
415
|
+
if (auth.role !== "admin")
|
|
416
|
+
return this.json(res, 403, { error: "forbidden" });
|
|
417
|
+
const body = await this.readJson(req);
|
|
418
|
+
const userId = String(body?.userId || "");
|
|
419
|
+
if (!userId)
|
|
420
|
+
return this.json(res, 400, { error: "missing_user_id" });
|
|
421
|
+
if (userId === auth.userId)
|
|
422
|
+
return this.json(res, 400, { error: "cannot_remove_self" });
|
|
423
|
+
if (userId === this.authState.bootstrapAdminUserId)
|
|
424
|
+
return this.json(res, 403, { error: "cannot_remove_owner", message: "The hub owner cannot be removed" });
|
|
425
|
+
const cleanResources = body?.cleanResources === true;
|
|
426
|
+
const deleted = this.opts.store.deleteHubUser(userId, cleanResources);
|
|
427
|
+
if (!deleted)
|
|
428
|
+
return this.json(res, 404, { error: "not_found" });
|
|
429
|
+
this.opts.log.info(`Hub: admin "${auth.userId}" removed user "${userId}" (cleanResources=${cleanResources})`);
|
|
430
|
+
return this.json(res, 200, { ok: true });
|
|
394
431
|
}
|
|
395
432
|
if (req.method === "POST" && routePath === "/api/v1/hub/tasks/share") {
|
|
396
433
|
const body = await this.readJson(req);
|
|
397
434
|
if (!body?.task)
|
|
398
435
|
return this.json(res, 400, { error: "invalid_payload" });
|
|
399
436
|
const task = { ...body.task, sourceUserId: auth.userId };
|
|
437
|
+
const existingTask = task.sourceTaskId ? this.opts.store.getHubTaskBySource(auth.userId, task.sourceTaskId) : null;
|
|
400
438
|
this.opts.store.upsertHubTask(task);
|
|
401
439
|
const chunks = Array.isArray(body.chunks) ? body.chunks : [];
|
|
402
440
|
const chunkIds = [];
|
|
@@ -404,15 +442,22 @@ class HubServer {
|
|
|
404
442
|
this.opts.store.upsertHubChunk({ ...chunk, sourceUserId: auth.userId });
|
|
405
443
|
chunkIds.push(chunk.id);
|
|
406
444
|
}
|
|
407
|
-
// Async embedding: don't block the response
|
|
408
445
|
if (this.opts.embedder && chunkIds.length > 0) {
|
|
409
446
|
this.embedChunksAsync(chunkIds, chunks);
|
|
410
447
|
}
|
|
448
|
+
if (!existingTask) {
|
|
449
|
+
this.notifyAdmins("resource_shared", "task", String(task.title || task.sourceTaskId || ""), auth.userId);
|
|
450
|
+
}
|
|
411
451
|
return this.json(res, 200, { ok: true, chunks: chunkIds.length });
|
|
412
452
|
}
|
|
413
453
|
if (req.method === "POST" && routePath === "/api/v1/hub/tasks/unshare") {
|
|
414
454
|
const body = await this.readJson(req);
|
|
415
|
-
|
|
455
|
+
const srcTaskId = String(body.sourceTaskId);
|
|
456
|
+
const existing = this.opts.store.getHubTaskBySource(auth.userId, srcTaskId);
|
|
457
|
+
this.opts.store.deleteHubTaskBySource(auth.userId, srcTaskId);
|
|
458
|
+
if (existing) {
|
|
459
|
+
this.notifyAdmins("resource_unshared", "task", existing.title || srcTaskId, auth.userId);
|
|
460
|
+
}
|
|
416
461
|
return this.json(res, 200, { ok: true });
|
|
417
462
|
}
|
|
418
463
|
if (req.method === "POST" && routePath === "/api/v1/hub/memories/share") {
|
|
@@ -444,6 +489,9 @@ class HubServer {
|
|
|
444
489
|
if (this.opts.embedder) {
|
|
445
490
|
this.embedMemoryAsync(memoryId, String(m.summary || ""), String(m.content || ""));
|
|
446
491
|
}
|
|
492
|
+
if (!existing) {
|
|
493
|
+
this.notifyAdmins("resource_shared", "memory", String(m.summary || m.content?.slice(0, 60) || memoryId), auth.userId);
|
|
494
|
+
}
|
|
447
495
|
return this.json(res, 200, { ok: true, memoryId, visibility });
|
|
448
496
|
}
|
|
449
497
|
if (req.method === "POST" && routePath === "/api/v1/hub/memories/unshare") {
|
|
@@ -451,7 +499,11 @@ class HubServer {
|
|
|
451
499
|
const sourceChunkId = String(body?.sourceChunkId || "");
|
|
452
500
|
if (!sourceChunkId)
|
|
453
501
|
return this.json(res, 400, { error: "missing_source_chunk_id" });
|
|
502
|
+
const existing = this.opts.store.getHubMemoryBySource(auth.userId, sourceChunkId);
|
|
454
503
|
this.opts.store.deleteHubMemoryBySource(auth.userId, sourceChunkId);
|
|
504
|
+
if (existing) {
|
|
505
|
+
this.notifyAdmins("resource_unshared", "memory", existing.summary || existing.content?.slice(0, 60) || sourceChunkId, auth.userId);
|
|
506
|
+
}
|
|
455
507
|
return this.json(res, 200, { ok: true });
|
|
456
508
|
}
|
|
457
509
|
if (req.method === "GET" && routePath === "/api/v1/hub/memories") {
|
|
@@ -576,6 +628,7 @@ class HubServer {
|
|
|
576
628
|
visibility: hit.visibility,
|
|
577
629
|
groupName: hit.group_name,
|
|
578
630
|
ownerName: hit.owner_name || "unknown",
|
|
631
|
+
ownerStatus: hit.owner_status || "",
|
|
579
632
|
qualityScore: hit.quality_score,
|
|
580
633
|
}));
|
|
581
634
|
return this.json(res, 200, { hits });
|
|
@@ -603,6 +656,9 @@ class HubServer {
|
|
|
603
656
|
createdAt: existing?.createdAt ?? Date.now(),
|
|
604
657
|
updatedAt: Date.now(),
|
|
605
658
|
});
|
|
659
|
+
if (!existing) {
|
|
660
|
+
this.notifyAdmins("resource_shared", "skill", String(metadata.name || sourceSkillId), auth.userId);
|
|
661
|
+
}
|
|
606
662
|
return this.json(res, 200, { ok: true, skillId, visibility });
|
|
607
663
|
}
|
|
608
664
|
const skillBundleMatch = req.method === "GET" ? routePath.match(/^\/api\/v1\/hub\/skills\/([^/]+)\/bundle$/) : null;
|
|
@@ -624,7 +680,12 @@ class HubServer {
|
|
|
624
680
|
}
|
|
625
681
|
if (req.method === "POST" && routePath === "/api/v1/hub/skills/unpublish") {
|
|
626
682
|
const body = await this.readJson(req);
|
|
627
|
-
|
|
683
|
+
const srcSkillId = String(body?.sourceSkillId || "");
|
|
684
|
+
const existing = this.opts.store.getHubSkillBySource(auth.userId, srcSkillId);
|
|
685
|
+
this.opts.store.deleteHubSkillBySource(auth.userId, srcSkillId);
|
|
686
|
+
if (existing) {
|
|
687
|
+
this.notifyAdmins("resource_unshared", "skill", existing.name || srcSkillId, auth.userId);
|
|
688
|
+
}
|
|
628
689
|
return this.json(res, 200, { ok: true });
|
|
629
690
|
}
|
|
630
691
|
// ── Admin: shared tasks & skills management ──
|
|
@@ -634,14 +695,51 @@ class HubServer {
|
|
|
634
695
|
const tasks = this.opts.store.listAllHubTasks();
|
|
635
696
|
return this.json(res, 200, { tasks });
|
|
636
697
|
}
|
|
698
|
+
const hubTaskDetailMatch = req.method === "GET" ? routePath.match(/^\/api\/v1\/hub\/shared-tasks\/([^/]+)\/detail$/) : null;
|
|
699
|
+
if (hubTaskDetailMatch) {
|
|
700
|
+
const taskId = decodeURIComponent(hubTaskDetailMatch[1]);
|
|
701
|
+
const task = this.opts.store.getHubTaskById(taskId);
|
|
702
|
+
if (!task)
|
|
703
|
+
return this.json(res, 404, { error: "not_found" });
|
|
704
|
+
const chunks = this.opts.store.listHubChunksByTaskId(taskId);
|
|
705
|
+
return this.json(res, 200, {
|
|
706
|
+
id: task.id, title: task.title, summary: task.summary,
|
|
707
|
+
startedAt: task.createdAt, endedAt: task.updatedAt,
|
|
708
|
+
chunks: chunks.map(c => ({ role: c.role, content: c.content, summary: c.summary, kind: c.kind, createdAt: c.createdAt })),
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
const hubSkillDetailMatch = req.method === "GET" ? routePath.match(/^\/api\/v1\/hub\/shared-skills\/([^/]+)\/detail$/) : null;
|
|
712
|
+
if (hubSkillDetailMatch) {
|
|
713
|
+
const skillId = decodeURIComponent(hubSkillDetailMatch[1]);
|
|
714
|
+
const skill = this.opts.store.getHubSkillById(skillId);
|
|
715
|
+
if (!skill)
|
|
716
|
+
return this.json(res, 404, { error: "not_found" });
|
|
717
|
+
let files = [];
|
|
718
|
+
try {
|
|
719
|
+
const bundle = JSON.parse(skill.bundle || "{}");
|
|
720
|
+
if (Array.isArray(bundle.files)) {
|
|
721
|
+
files = bundle.files.map((f) => ({ path: f.path ?? f.name ?? "unknown", type: f.type ?? "file", size: f.size ?? (f.content ? f.content.length : 0) }));
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
catch { /* ignore parse error */ }
|
|
725
|
+
return this.json(res, 200, {
|
|
726
|
+
skill: { id: skill.id, name: skill.name, description: skill.description, version: skill.version, qualityScore: skill.qualityScore, status: "published" },
|
|
727
|
+
files,
|
|
728
|
+
versions: [],
|
|
729
|
+
});
|
|
730
|
+
}
|
|
637
731
|
const adminTaskDeleteMatch = req.method === "DELETE" ? routePath.match(/^\/api\/v1\/hub\/admin\/shared-tasks\/([^/]+)$/) : null;
|
|
638
732
|
if (adminTaskDeleteMatch) {
|
|
639
733
|
if (auth.role !== "admin")
|
|
640
734
|
return this.json(res, 403, { error: "forbidden" });
|
|
641
735
|
const taskId = decodeURIComponent(adminTaskDeleteMatch[1]);
|
|
736
|
+
const taskInfo = this.opts.store.getHubTaskById(taskId);
|
|
642
737
|
const deleted = this.opts.store.deleteHubTaskById(taskId);
|
|
643
738
|
if (!deleted)
|
|
644
739
|
return this.json(res, 404, { error: "not_found" });
|
|
740
|
+
if (taskInfo) {
|
|
741
|
+
this.opts.store.insertHubNotification({ id: (0, crypto_1.randomUUID)(), userId: taskInfo.sourceUserId, type: "resource_removed", resource: "task", title: taskInfo.title });
|
|
742
|
+
}
|
|
645
743
|
return this.json(res, 200, { ok: true });
|
|
646
744
|
}
|
|
647
745
|
if (req.method === "GET" && routePath === "/api/v1/hub/admin/shared-skills") {
|
|
@@ -655,9 +753,13 @@ class HubServer {
|
|
|
655
753
|
if (auth.role !== "admin")
|
|
656
754
|
return this.json(res, 403, { error: "forbidden" });
|
|
657
755
|
const skillId = decodeURIComponent(adminSkillDeleteMatch[1]);
|
|
756
|
+
const skillInfo = this.opts.store.getHubSkillById(skillId);
|
|
658
757
|
const deleted = this.opts.store.deleteHubSkillById(skillId);
|
|
659
758
|
if (!deleted)
|
|
660
759
|
return this.json(res, 404, { error: "not_found" });
|
|
760
|
+
if (skillInfo) {
|
|
761
|
+
this.opts.store.insertHubNotification({ id: (0, crypto_1.randomUUID)(), userId: skillInfo.sourceUserId, type: "resource_removed", resource: "skill", title: skillInfo.name });
|
|
762
|
+
}
|
|
661
763
|
return this.json(res, 200, { ok: true });
|
|
662
764
|
}
|
|
663
765
|
if (req.method === "GET" && routePath === "/api/v1/hub/admin/shared-memories") {
|
|
@@ -671,9 +773,13 @@ class HubServer {
|
|
|
671
773
|
if (auth.role !== "admin")
|
|
672
774
|
return this.json(res, 403, { error: "forbidden" });
|
|
673
775
|
const memoryId = decodeURIComponent(adminMemoryDeleteMatch[1]);
|
|
776
|
+
const memInfo = this.opts.store.getHubMemoryById(memoryId);
|
|
674
777
|
const deleted = this.opts.store.deleteHubMemoryById(memoryId);
|
|
675
778
|
if (!deleted)
|
|
676
779
|
return this.json(res, 404, { error: "not_found" });
|
|
780
|
+
if (memInfo) {
|
|
781
|
+
this.opts.store.insertHubNotification({ id: (0, crypto_1.randomUUID)(), userId: memInfo.sourceUserId, type: "resource_removed", resource: "memory", title: memInfo.summary || memInfo.id });
|
|
782
|
+
}
|
|
677
783
|
return this.json(res, 200, { ok: true });
|
|
678
784
|
}
|
|
679
785
|
if (req.method === "POST" && routePath === "/api/v1/hub/memory-detail") {
|
|
@@ -698,8 +804,85 @@ class HubServer {
|
|
|
698
804
|
source: { ts: chunk.createdAt, role: chunk.role },
|
|
699
805
|
});
|
|
700
806
|
}
|
|
807
|
+
if (req.method === "GET" && routePath === "/api/v1/hub/notifications") {
|
|
808
|
+
const unread = (new URL(req.url, `http://${req.headers.host}`)).searchParams.get("unread") === "1";
|
|
809
|
+
const list = this.opts.store.listHubNotifications(auth.userId, { unreadOnly: unread, limit: 50 });
|
|
810
|
+
const unreadCount = this.opts.store.countUnreadHubNotifications(auth.userId);
|
|
811
|
+
return this.json(res, 200, { notifications: list, unreadCount });
|
|
812
|
+
}
|
|
813
|
+
if (req.method === "POST" && routePath === "/api/v1/hub/notifications/read") {
|
|
814
|
+
const body = await this.readJson(req);
|
|
815
|
+
const ids = Array.isArray(body.ids) ? body.ids : undefined;
|
|
816
|
+
this.opts.store.markHubNotificationsRead(auth.userId, ids);
|
|
817
|
+
return this.json(res, 200, { ok: true });
|
|
818
|
+
}
|
|
819
|
+
if (req.method === "POST" && routePath === "/api/v1/hub/notifications/clear") {
|
|
820
|
+
this.opts.store.clearHubNotifications(auth.userId);
|
|
821
|
+
return this.json(res, 200, { ok: true });
|
|
822
|
+
}
|
|
701
823
|
return this.json(res, 404, { error: "not_found" });
|
|
702
824
|
}
|
|
825
|
+
notifyAdmins(type, resource, title, fromUserId, opts) {
|
|
826
|
+
try {
|
|
827
|
+
const admins = this.opts.store.listHubUsers("active").filter(u => u.role === "admin" && u.id !== fromUserId);
|
|
828
|
+
for (const admin of admins) {
|
|
829
|
+
if (opts?.dedup && this.opts.store.hasRecentHubNotification(admin.id, type, resource, opts.deduoWindowMs ?? 300_000)) {
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
this.opts.store.insertHubNotification({ id: (0, crypto_1.randomUUID)(), userId: admin.id, type, resource, title });
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
catch { /* best-effort */ }
|
|
836
|
+
}
|
|
837
|
+
initOnlineTracking() {
|
|
838
|
+
try {
|
|
839
|
+
const ownerId = this.authState.bootstrapAdminUserId || "";
|
|
840
|
+
const users = this.opts.store.listHubUsers("active");
|
|
841
|
+
const now = Date.now();
|
|
842
|
+
for (const u of users) {
|
|
843
|
+
if (u.id === ownerId)
|
|
844
|
+
continue;
|
|
845
|
+
if (u.lastActiveAt && now - u.lastActiveAt < HubServer.OFFLINE_THRESHOLD_MS) {
|
|
846
|
+
this.knownOnlineUsers.add(u.id);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
catch { /* best-effort */ }
|
|
851
|
+
}
|
|
852
|
+
checkOfflineUsers() {
|
|
853
|
+
try {
|
|
854
|
+
const ownerId = this.authState.bootstrapAdminUserId || "";
|
|
855
|
+
const users = this.opts.store.listHubUsers("active");
|
|
856
|
+
const now = Date.now();
|
|
857
|
+
const currentlyOnline = new Set();
|
|
858
|
+
for (const u of users) {
|
|
859
|
+
if (u.id === ownerId)
|
|
860
|
+
continue;
|
|
861
|
+
if (u.lastActiveAt && now - u.lastActiveAt < HubServer.OFFLINE_THRESHOLD_MS) {
|
|
862
|
+
currentlyOnline.add(u.id);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
for (const uid of this.knownOnlineUsers) {
|
|
866
|
+
if (!currentlyOnline.has(uid)) {
|
|
867
|
+
const user = users.find(u => u.id === uid);
|
|
868
|
+
if (user) {
|
|
869
|
+
this.notifyAdmins("user_offline", "user", user.username, uid);
|
|
870
|
+
this.opts.log.info(`Hub: user "${user.username}" (${uid}) went offline`);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
for (const uid of currentlyOnline) {
|
|
875
|
+
if (!this.knownOnlineUsers.has(uid)) {
|
|
876
|
+
const user = users.find(u => u.id === uid);
|
|
877
|
+
if (user) {
|
|
878
|
+
this.notifyAdmins("user_online", "user", user.username, uid);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
this.knownOnlineUsers = currentlyOnline;
|
|
883
|
+
}
|
|
884
|
+
catch { /* best-effort */ }
|
|
885
|
+
}
|
|
703
886
|
authenticate(req) {
|
|
704
887
|
const header = req.headers.authorization;
|
|
705
888
|
if (!header || !header.startsWith("Bearer "))
|
|
@@ -714,6 +897,13 @@ class HubServer {
|
|
|
714
897
|
const hash = (0, crypto_1.createHash)("sha256").update(token).digest("hex");
|
|
715
898
|
if (user.tokenHash !== hash)
|
|
716
899
|
return null;
|
|
900
|
+
const clientIp = req.headers["x-client-ip"]?.trim()
|
|
901
|
+
|| req.headers["x-forwarded-for"]?.split(",")[0]?.trim()
|
|
902
|
+
|| req.socket.remoteAddress || "";
|
|
903
|
+
try {
|
|
904
|
+
this.opts.store.updateHubUserActivity(user.id, clientIp);
|
|
905
|
+
}
|
|
906
|
+
catch { /* best-effort */ }
|
|
717
907
|
return {
|
|
718
908
|
userId: user.id,
|
|
719
909
|
username: user.username,
|