@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/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,14 +203,53 @@ 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 || "";
|
|
210
|
+
const existingUsers = this.opts.store.listHubUsers();
|
|
211
|
+
const existingUser = existingUsers.find(u => u.username === username);
|
|
212
|
+
if (existingUser) {
|
|
213
|
+
try {
|
|
214
|
+
this.opts.store.updateHubUserActivity(existingUser.id, joinIp);
|
|
215
|
+
}
|
|
216
|
+
catch { /* best-effort */ }
|
|
217
|
+
if (existingUser.status === "active") {
|
|
218
|
+
const token = (0, auth_1.issueUserToken)({ userId: existingUser.id, username: existingUser.username, role: existingUser.role, status: "active" }, this.authSecret);
|
|
219
|
+
this.userManager.approveUser(existingUser.id, token);
|
|
220
|
+
return this.json(res, 200, { status: "active", userId: existingUser.id, userToken: token });
|
|
221
|
+
}
|
|
222
|
+
if (existingUser.status === "pending") {
|
|
223
|
+
this.notifyAdmins("user_join_request", "user", username, "", { dedup: true });
|
|
224
|
+
return this.json(res, 200, { status: "pending", userId: existingUser.id });
|
|
225
|
+
}
|
|
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") {
|
|
236
|
+
this.userManager.resetToPending(existingUser.id);
|
|
237
|
+
this.notifyAdmins("user_join_request", "user", username, "");
|
|
238
|
+
this.opts.log.info(`Hub: removed user "${username}" (${existingUser.id}) re-applied, reset to pending`);
|
|
239
|
+
return this.json(res, 200, { status: "pending", userId: existingUser.id });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
196
242
|
const user = this.userManager.createPendingUser({
|
|
197
243
|
username,
|
|
198
244
|
deviceName: typeof body.deviceName === "string" ? body.deviceName : undefined,
|
|
199
245
|
});
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
246
|
+
try {
|
|
247
|
+
this.opts.store.updateHubUserActivity(user.id, joinIp);
|
|
248
|
+
}
|
|
249
|
+
catch { /* best-effort */ }
|
|
250
|
+
this.opts.log.info(`Hub: user "${username}" (${user.id}) registered as pending, awaiting admin approval`);
|
|
251
|
+
this.notifyAdmins("user_join_request", "user", username, "");
|
|
252
|
+
return this.json(res, 200, { status: "pending", userId: user.id });
|
|
204
253
|
}
|
|
205
254
|
if (req.method === "POST" && routePath === "/api/v1/hub/registration-status") {
|
|
206
255
|
const body = await this.readJson(req);
|
|
@@ -233,6 +282,19 @@ class HubServer {
|
|
|
233
282
|
if (!this.checkRateLimit(auth.userId, endpointKey)) {
|
|
234
283
|
return this.json(res, 429, { error: "rate_limit_exceeded", retryAfterMs: HubServer.RATE_WINDOW_MS });
|
|
235
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
|
+
}
|
|
236
298
|
if (req.method === "GET" && routePath === "/api/v1/hub/me") {
|
|
237
299
|
const user = this.opts.store.getHubUser(auth.userId);
|
|
238
300
|
if (!user)
|
|
@@ -253,7 +315,8 @@ class HubServer {
|
|
|
253
315
|
const updated = this.userManager.updateUsername(auth.userId, newUsername);
|
|
254
316
|
if (!updated)
|
|
255
317
|
return this.json(res, 404, { error: "not_found" });
|
|
256
|
-
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);
|
|
257
320
|
this.userManager.approveUser(updated.id, newToken);
|
|
258
321
|
this.opts.log.info(`Hub: user "${auth.userId}" renamed to "${newUsername}"`);
|
|
259
322
|
return this.json(res, 200, { ok: true, username: newUsername, userToken: newToken });
|
|
@@ -271,6 +334,10 @@ class HubServer {
|
|
|
271
334
|
const approved = this.userManager.approveUser(String(body.userId), token);
|
|
272
335
|
if (!approved)
|
|
273
336
|
return this.json(res, 404, { error: "not_found" });
|
|
337
|
+
try {
|
|
338
|
+
this.opts.store.updateHubUserActivity(String(body.userId), "");
|
|
339
|
+
}
|
|
340
|
+
catch { /* best-effort */ }
|
|
274
341
|
return this.json(res, 200, { status: "active", token });
|
|
275
342
|
}
|
|
276
343
|
if (req.method === "POST" && routePath === "/api/v1/hub/admin/reject-user") {
|
|
@@ -286,98 +353,88 @@ class HubServer {
|
|
|
286
353
|
if (auth.role !== "admin")
|
|
287
354
|
return this.json(res, 403, { error: "forbidden" });
|
|
288
355
|
const users = this.opts.store.listHubUsers().filter(u => u.status === "active");
|
|
289
|
-
|
|
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
|
+
}) });
|
|
290
370
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
371
|
+
if (req.method === "POST" && routePath === "/api/v1/hub/admin/change-role") {
|
|
372
|
+
if (auth.role !== "admin")
|
|
373
|
+
return this.json(res, 403, { error: "forbidden" });
|
|
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 });
|
|
295
389
|
}
|
|
296
|
-
if (req.method === "POST" && routePath === "/api/v1/hub/
|
|
390
|
+
if (req.method === "POST" && routePath === "/api/v1/hub/admin/rename-user") {
|
|
297
391
|
if (auth.role !== "admin")
|
|
298
392
|
return this.json(res, 403, { error: "forbidden" });
|
|
299
393
|
const body = await this.readJson(req);
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
this.opts.store.upsertHubGroup({
|
|
305
|
-
id: groupId,
|
|
306
|
-
name,
|
|
307
|
-
description: String(body.description || ""),
|
|
308
|
-
createdAt: Date.now(),
|
|
309
|
-
});
|
|
310
|
-
return this.json(res, 201, { id: groupId, name });
|
|
311
|
-
}
|
|
312
|
-
const groupDetailMatch = routePath.match(/^\/api\/v1\/hub\/groups\/([^/]+)$/);
|
|
313
|
-
if (groupDetailMatch) {
|
|
314
|
-
const groupId = decodeURIComponent(groupDetailMatch[1]);
|
|
315
|
-
if (req.method === "GET") {
|
|
316
|
-
const group = this.opts.store.getHubGroupById(groupId);
|
|
317
|
-
if (!group)
|
|
318
|
-
return this.json(res, 404, { error: "not_found" });
|
|
319
|
-
const members = this.opts.store.listHubGroupMembers(groupId);
|
|
320
|
-
return this.json(res, 200, { ...group, members });
|
|
321
|
-
}
|
|
322
|
-
if (req.method === "PUT") {
|
|
323
|
-
if (auth.role !== "admin")
|
|
324
|
-
return this.json(res, 403, { error: "forbidden" });
|
|
325
|
-
const existing = this.opts.store.getHubGroupById(groupId);
|
|
326
|
-
if (!existing)
|
|
327
|
-
return this.json(res, 404, { error: "not_found" });
|
|
328
|
-
const body = await this.readJson(req);
|
|
329
|
-
this.opts.store.upsertHubGroup({
|
|
330
|
-
id: groupId,
|
|
331
|
-
name: String(body.name || existing.name).trim(),
|
|
332
|
-
description: String(body.description ?? existing.description),
|
|
333
|
-
createdAt: existing.createdAt,
|
|
334
|
-
});
|
|
335
|
-
return this.json(res, 200, { ok: true });
|
|
336
|
-
}
|
|
337
|
-
if (req.method === "DELETE") {
|
|
338
|
-
if (auth.role !== "admin")
|
|
339
|
-
return this.json(res, 403, { error: "forbidden" });
|
|
340
|
-
const deleted = this.opts.store.deleteHubGroup(groupId);
|
|
341
|
-
if (!deleted)
|
|
342
|
-
return this.json(res, 404, { error: "not_found" });
|
|
343
|
-
return this.json(res, 200, { ok: true });
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
const groupMembersMatch = routePath.match(/^\/api\/v1\/hub\/groups\/([^/]+)\/members$/);
|
|
347
|
-
if (groupMembersMatch) {
|
|
348
|
-
const groupId = decodeURIComponent(groupMembersMatch[1]);
|
|
349
|
-
if (req.method === "POST") {
|
|
350
|
-
if (auth.role !== "admin")
|
|
351
|
-
return this.json(res, 403, { error: "forbidden" });
|
|
352
|
-
const group = this.opts.store.getHubGroupById(groupId);
|
|
353
|
-
if (!group)
|
|
354
|
-
return this.json(res, 404, { error: "group_not_found" });
|
|
355
|
-
const body = await this.readJson(req);
|
|
356
|
-
const userId = String(body.userId || "");
|
|
357
|
-
if (!userId)
|
|
358
|
-
return this.json(res, 400, { error: "userId_required" });
|
|
359
|
-
const user = this.opts.store.getHubUser(userId);
|
|
360
|
-
if (!user)
|
|
361
|
-
return this.json(res, 404, { error: "user_not_found" });
|
|
362
|
-
this.opts.store.addHubGroupMember(groupId, userId);
|
|
363
|
-
return this.json(res, 200, { ok: true });
|
|
364
|
-
}
|
|
365
|
-
if (req.method === "DELETE") {
|
|
366
|
-
if (auth.role !== "admin")
|
|
367
|
-
return this.json(res, 403, { error: "forbidden" });
|
|
368
|
-
const body = await this.readJson(req);
|
|
369
|
-
const userId = String(body.userId || "");
|
|
370
|
-
if (!userId)
|
|
371
|
-
return this.json(res, 400, { error: "userId_required" });
|
|
372
|
-
this.opts.store.removeHubGroupMember(groupId, userId);
|
|
373
|
-
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" });
|
|
374
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 });
|
|
375
431
|
}
|
|
376
432
|
if (req.method === "POST" && routePath === "/api/v1/hub/tasks/share") {
|
|
377
433
|
const body = await this.readJson(req);
|
|
378
434
|
if (!body?.task)
|
|
379
435
|
return this.json(res, 400, { error: "invalid_payload" });
|
|
380
436
|
const task = { ...body.task, sourceUserId: auth.userId };
|
|
437
|
+
const existingTask = task.sourceTaskId ? this.opts.store.getHubTaskBySource(auth.userId, task.sourceTaskId) : null;
|
|
381
438
|
this.opts.store.upsertHubTask(task);
|
|
382
439
|
const chunks = Array.isArray(body.chunks) ? body.chunks : [];
|
|
383
440
|
const chunkIds = [];
|
|
@@ -385,15 +442,22 @@ class HubServer {
|
|
|
385
442
|
this.opts.store.upsertHubChunk({ ...chunk, sourceUserId: auth.userId });
|
|
386
443
|
chunkIds.push(chunk.id);
|
|
387
444
|
}
|
|
388
|
-
// Async embedding: don't block the response
|
|
389
445
|
if (this.opts.embedder && chunkIds.length > 0) {
|
|
390
446
|
this.embedChunksAsync(chunkIds, chunks);
|
|
391
447
|
}
|
|
448
|
+
if (!existingTask) {
|
|
449
|
+
this.notifyAdmins("resource_shared", "task", String(task.title || task.sourceTaskId || ""), auth.userId);
|
|
450
|
+
}
|
|
392
451
|
return this.json(res, 200, { ok: true, chunks: chunkIds.length });
|
|
393
452
|
}
|
|
394
453
|
if (req.method === "POST" && routePath === "/api/v1/hub/tasks/unshare") {
|
|
395
454
|
const body = await this.readJson(req);
|
|
396
|
-
|
|
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
|
+
}
|
|
397
461
|
return this.json(res, 200, { ok: true });
|
|
398
462
|
}
|
|
399
463
|
if (req.method === "POST" && routePath === "/api/v1/hub/memories/share") {
|
|
@@ -406,17 +470,8 @@ class HubServer {
|
|
|
406
470
|
return this.json(res, 400, { error: "missing_source_chunk_id" });
|
|
407
471
|
const existing = this.opts.store.getHubMemoryBySource(auth.userId, sourceChunkId);
|
|
408
472
|
const memoryId = existing?.id ?? (0, crypto_1.randomUUID)();
|
|
409
|
-
const visibility =
|
|
410
|
-
|
|
411
|
-
if (visibility === "group") {
|
|
412
|
-
const gid = String(m.groupId || "");
|
|
413
|
-
if (!gid)
|
|
414
|
-
return this.json(res, 400, { error: "missing_group_id" });
|
|
415
|
-
const group = this.opts.store.getHubGroupById(gid);
|
|
416
|
-
if (!group)
|
|
417
|
-
return this.json(res, 404, { error: "group_not_found" });
|
|
418
|
-
resolvedGroupId = gid;
|
|
419
|
-
}
|
|
473
|
+
const visibility = "public";
|
|
474
|
+
const resolvedGroupId = null;
|
|
420
475
|
const now = Date.now();
|
|
421
476
|
this.opts.store.upsertHubMemory({
|
|
422
477
|
id: memoryId,
|
|
@@ -434,6 +489,9 @@ class HubServer {
|
|
|
434
489
|
if (this.opts.embedder) {
|
|
435
490
|
this.embedMemoryAsync(memoryId, String(m.summary || ""), String(m.content || ""));
|
|
436
491
|
}
|
|
492
|
+
if (!existing) {
|
|
493
|
+
this.notifyAdmins("resource_shared", "memory", String(m.summary || m.content?.slice(0, 60) || memoryId), auth.userId);
|
|
494
|
+
}
|
|
437
495
|
return this.json(res, 200, { ok: true, memoryId, visibility });
|
|
438
496
|
}
|
|
439
497
|
if (req.method === "POST" && routePath === "/api/v1/hub/memories/unshare") {
|
|
@@ -441,7 +499,11 @@ class HubServer {
|
|
|
441
499
|
const sourceChunkId = String(body?.sourceChunkId || "");
|
|
442
500
|
if (!sourceChunkId)
|
|
443
501
|
return this.json(res, 400, { error: "missing_source_chunk_id" });
|
|
502
|
+
const existing = this.opts.store.getHubMemoryBySource(auth.userId, sourceChunkId);
|
|
444
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
|
+
}
|
|
445
507
|
return this.json(res, 200, { ok: true });
|
|
446
508
|
}
|
|
447
509
|
if (req.method === "GET" && routePath === "/api/v1/hub/memories") {
|
|
@@ -566,6 +628,7 @@ class HubServer {
|
|
|
566
628
|
visibility: hit.visibility,
|
|
567
629
|
groupName: hit.group_name,
|
|
568
630
|
ownerName: hit.owner_name || "unknown",
|
|
631
|
+
ownerStatus: hit.owner_status || "",
|
|
569
632
|
qualityScore: hit.quality_score,
|
|
570
633
|
}));
|
|
571
634
|
return this.json(res, 200, { hits });
|
|
@@ -578,7 +641,7 @@ class HubServer {
|
|
|
578
641
|
return this.json(res, 400, { error: "missing_skill_id" });
|
|
579
642
|
const existing = this.opts.store.getHubSkillBySource(auth.userId, sourceSkillId);
|
|
580
643
|
const skillId = existing?.id ?? (0, crypto_1.randomUUID)();
|
|
581
|
-
const visibility =
|
|
644
|
+
const visibility = "public";
|
|
582
645
|
this.opts.store.upsertHubSkill({
|
|
583
646
|
id: skillId,
|
|
584
647
|
sourceSkillId,
|
|
@@ -586,13 +649,16 @@ class HubServer {
|
|
|
586
649
|
name: String(metadata.name || sourceSkillId),
|
|
587
650
|
description: String(metadata.description || ""),
|
|
588
651
|
version: Number(metadata.version || 1),
|
|
589
|
-
groupId:
|
|
652
|
+
groupId: null,
|
|
590
653
|
visibility,
|
|
591
654
|
bundle: JSON.stringify(body?.bundle ?? {}),
|
|
592
655
|
qualityScore: metadata.qualityScore == null ? null : Number(metadata.qualityScore),
|
|
593
656
|
createdAt: existing?.createdAt ?? Date.now(),
|
|
594
657
|
updatedAt: Date.now(),
|
|
595
658
|
});
|
|
659
|
+
if (!existing) {
|
|
660
|
+
this.notifyAdmins("resource_shared", "skill", String(metadata.name || sourceSkillId), auth.userId);
|
|
661
|
+
}
|
|
596
662
|
return this.json(res, 200, { ok: true, skillId, visibility });
|
|
597
663
|
}
|
|
598
664
|
const skillBundleMatch = req.method === "GET" ? routePath.match(/^\/api\/v1\/hub\/skills\/([^/]+)\/bundle$/) : null;
|
|
@@ -600,11 +666,6 @@ class HubServer {
|
|
|
600
666
|
const skill = this.opts.store.getHubSkillById(decodeURIComponent(skillBundleMatch[1]));
|
|
601
667
|
if (!skill)
|
|
602
668
|
return this.json(res, 404, { error: "not_found" });
|
|
603
|
-
const user = this.opts.store.getHubUser(auth.userId);
|
|
604
|
-
const groups = new Set((user?.groups ?? []).map((group) => group.id));
|
|
605
|
-
const allowed = skill.visibility === "public" || (skill.groupId != null && groups.has(skill.groupId));
|
|
606
|
-
if (!allowed)
|
|
607
|
-
return this.json(res, 403, { error: "forbidden" });
|
|
608
669
|
return this.json(res, 200, {
|
|
609
670
|
skillId: skill.id,
|
|
610
671
|
metadata: {
|
|
@@ -619,7 +680,12 @@ class HubServer {
|
|
|
619
680
|
}
|
|
620
681
|
if (req.method === "POST" && routePath === "/api/v1/hub/skills/unpublish") {
|
|
621
682
|
const body = await this.readJson(req);
|
|
622
|
-
|
|
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
|
+
}
|
|
623
689
|
return this.json(res, 200, { ok: true });
|
|
624
690
|
}
|
|
625
691
|
// ── Admin: shared tasks & skills management ──
|
|
@@ -629,14 +695,51 @@ class HubServer {
|
|
|
629
695
|
const tasks = this.opts.store.listAllHubTasks();
|
|
630
696
|
return this.json(res, 200, { tasks });
|
|
631
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
|
+
}
|
|
632
731
|
const adminTaskDeleteMatch = req.method === "DELETE" ? routePath.match(/^\/api\/v1\/hub\/admin\/shared-tasks\/([^/]+)$/) : null;
|
|
633
732
|
if (adminTaskDeleteMatch) {
|
|
634
733
|
if (auth.role !== "admin")
|
|
635
734
|
return this.json(res, 403, { error: "forbidden" });
|
|
636
735
|
const taskId = decodeURIComponent(adminTaskDeleteMatch[1]);
|
|
736
|
+
const taskInfo = this.opts.store.getHubTaskById(taskId);
|
|
637
737
|
const deleted = this.opts.store.deleteHubTaskById(taskId);
|
|
638
738
|
if (!deleted)
|
|
639
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
|
+
}
|
|
640
743
|
return this.json(res, 200, { ok: true });
|
|
641
744
|
}
|
|
642
745
|
if (req.method === "GET" && routePath === "/api/v1/hub/admin/shared-skills") {
|
|
@@ -650,9 +753,13 @@ class HubServer {
|
|
|
650
753
|
if (auth.role !== "admin")
|
|
651
754
|
return this.json(res, 403, { error: "forbidden" });
|
|
652
755
|
const skillId = decodeURIComponent(adminSkillDeleteMatch[1]);
|
|
756
|
+
const skillInfo = this.opts.store.getHubSkillById(skillId);
|
|
653
757
|
const deleted = this.opts.store.deleteHubSkillById(skillId);
|
|
654
758
|
if (!deleted)
|
|
655
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
|
+
}
|
|
656
763
|
return this.json(res, 200, { ok: true });
|
|
657
764
|
}
|
|
658
765
|
if (req.method === "GET" && routePath === "/api/v1/hub/admin/shared-memories") {
|
|
@@ -666,9 +773,13 @@ class HubServer {
|
|
|
666
773
|
if (auth.role !== "admin")
|
|
667
774
|
return this.json(res, 403, { error: "forbidden" });
|
|
668
775
|
const memoryId = decodeURIComponent(adminMemoryDeleteMatch[1]);
|
|
776
|
+
const memInfo = this.opts.store.getHubMemoryById(memoryId);
|
|
669
777
|
const deleted = this.opts.store.deleteHubMemoryById(memoryId);
|
|
670
778
|
if (!deleted)
|
|
671
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
|
+
}
|
|
672
783
|
return this.json(res, 200, { ok: true });
|
|
673
784
|
}
|
|
674
785
|
if (req.method === "POST" && routePath === "/api/v1/hub/memory-detail") {
|
|
@@ -693,8 +804,85 @@ class HubServer {
|
|
|
693
804
|
source: { ts: chunk.createdAt, role: chunk.role },
|
|
694
805
|
});
|
|
695
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
|
+
}
|
|
696
823
|
return this.json(res, 404, { error: "not_found" });
|
|
697
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
|
+
}
|
|
698
886
|
authenticate(req) {
|
|
699
887
|
const header = req.headers.authorization;
|
|
700
888
|
if (!header || !header.startsWith("Bearer "))
|
|
@@ -709,6 +897,13 @@ class HubServer {
|
|
|
709
897
|
const hash = (0, crypto_1.createHash)("sha256").update(token).digest("hex");
|
|
710
898
|
if (user.tokenHash !== hash)
|
|
711
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 */ }
|
|
712
907
|
return {
|
|
713
908
|
userId: user.id,
|
|
714
909
|
username: user.username,
|