@memtensor/memos-local-openclaw-plugin 1.0.4-beta.8 → 1.0.4
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 +94 -27
- package/dist/capture/index.js +3 -1
- package/dist/capture/index.js.map +1 -1
- package/dist/client/connector.d.ts +5 -0
- package/dist/client/connector.d.ts.map +1 -1
- package/dist/client/connector.js +132 -10
- package/dist/client/connector.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -1
- package/dist/config.js.map +1 -1
- package/dist/hub/server.d.ts +2 -0
- package/dist/hub/server.d.ts.map +1 -1
- package/dist/hub/server.js +251 -38
- package/dist/hub/server.js.map +1 -1
- package/dist/hub/user-manager.d.ts +9 -0
- package/dist/hub/user-manager.d.ts.map +1 -1
- package/dist/hub/user-manager.js +26 -2
- package/dist/hub/user-manager.js.map +1 -1
- package/dist/ingest/chunker.d.ts +2 -1
- package/dist/ingest/chunker.d.ts.map +1 -1
- package/dist/ingest/chunker.js +14 -10
- package/dist/ingest/chunker.js.map +1 -1
- package/dist/ingest/providers/index.js +2 -2
- package/dist/ingest/providers/index.js.map +1 -1
- package/dist/recall/engine.d.ts.map +1 -1
- package/dist/recall/engine.js +96 -1
- package/dist/recall/engine.js.map +1 -1
- package/dist/shared/llm-call.d.ts.map +1 -1
- package/dist/shared/llm-call.js +2 -1
- 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 +56 -5
- package/dist/skill/evolver.js.map +1 -1
- package/dist/skill/generator.d.ts +2 -0
- package/dist/skill/generator.d.ts.map +1 -1
- package/dist/skill/generator.js +45 -3
- package/dist/skill/generator.js.map +1 -1
- package/dist/skill/installer.d.ts +26 -0
- package/dist/skill/installer.d.ts.map +1 -1
- package/dist/skill/installer.js +80 -4
- package/dist/skill/installer.js.map +1 -1
- package/dist/skill/upgrader.d.ts +2 -0
- package/dist/skill/upgrader.d.ts.map +1 -1
- package/dist/skill/upgrader.js +139 -1
- package/dist/skill/upgrader.js.map +1 -1
- package/dist/skill/validator.d.ts +3 -0
- package/dist/skill/validator.d.ts.map +1 -1
- package/dist/skill/validator.js +75 -0
- package/dist/skill/validator.js.map +1 -1
- package/dist/storage/sqlite.d.ts +58 -0
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +295 -35
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +27 -8
- package/dist/telemetry.js.map +1 -1
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -1
- package/dist/viewer/html.d.ts.map +1 -1
- package/dist/viewer/html.js +796 -289
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +11 -0
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +456 -92
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +411 -52
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
- package/prebuilds/darwin-arm64/better_sqlite3.node +0 -0
- package/prebuilds/darwin-x64/better_sqlite3.node +0 -0
- package/prebuilds/linux-x64/better_sqlite3.node +0 -0
- package/prebuilds/win32-x64/better_sqlite3.node +0 -0
- package/src/capture/index.ts +4 -1
- package/src/client/connector.ts +136 -10
- package/src/config.ts +2 -1
- package/src/hub/server.ts +246 -38
- package/src/hub/user-manager.ts +42 -6
- package/src/ingest/chunker.ts +19 -13
- package/src/ingest/providers/index.ts +2 -2
- package/src/recall/engine.ts +89 -1
- package/src/shared/llm-call.ts +2 -1
- package/src/sharing/types.ts +1 -1
- package/src/skill/evolver.ts +58 -6
- package/src/skill/generator.ts +44 -5
- package/src/skill/installer.ts +107 -4
- package/src/skill/upgrader.ts +139 -1
- package/src/skill/validator.ts +79 -0
- package/src/storage/sqlite.ts +326 -40
- package/src/telemetry.ts +27 -9
- package/src/types.ts +11 -0
- package/src/viewer/html.ts +796 -289
- package/src/viewer/server.ts +430 -89
- package/telemetry.credentials.json +5 -0
package/src/hub/server.ts
CHANGED
|
@@ -14,6 +14,7 @@ type HubServerOptions = {
|
|
|
14
14
|
config: MemosLocalConfig;
|
|
15
15
|
dataDir: string;
|
|
16
16
|
embedder?: Embedder;
|
|
17
|
+
defaultHubPort?: number;
|
|
17
18
|
};
|
|
18
19
|
|
|
19
20
|
type HubAuthState = {
|
|
@@ -79,18 +80,31 @@ export class HubServer {
|
|
|
79
80
|
}
|
|
80
81
|
});
|
|
81
82
|
|
|
83
|
+
const MAX_PORT_RETRIES = 3;
|
|
84
|
+
let hubPort = this.port;
|
|
82
85
|
await new Promise<void>((resolve, reject) => {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
+
let retries = 0;
|
|
87
|
+
const onError = (err: NodeJS.ErrnoException) => {
|
|
88
|
+
if (err.code === "EADDRINUSE" && retries < MAX_PORT_RETRIES) {
|
|
89
|
+
retries++;
|
|
90
|
+
hubPort = this.port + retries;
|
|
91
|
+
this.opts.log.warn(`Hub port ${hubPort - 1} in use, trying ${hubPort}`);
|
|
92
|
+
this.server!.listen(hubPort, "0.0.0.0");
|
|
93
|
+
} else {
|
|
94
|
+
this.server?.off("listening", onListening);
|
|
95
|
+
reject(err);
|
|
96
|
+
}
|
|
86
97
|
};
|
|
87
98
|
const onListening = () => {
|
|
88
99
|
this.server?.off("error", onError);
|
|
100
|
+
if (hubPort !== this.port) {
|
|
101
|
+
this.opts.log.info(`Hub started on fallback port ${hubPort} (configured: ${this.port})`);
|
|
102
|
+
}
|
|
89
103
|
resolve();
|
|
90
104
|
};
|
|
91
|
-
this.server!.
|
|
105
|
+
this.server!.on("error", onError);
|
|
92
106
|
this.server!.once("listening", onListening);
|
|
93
|
-
this.server!.listen(
|
|
107
|
+
this.server!.listen(hubPort, "0.0.0.0");
|
|
94
108
|
});
|
|
95
109
|
|
|
96
110
|
const bootstrap = this.userManager.ensureBootstrapAdmin(
|
|
@@ -109,19 +123,37 @@ export class HubServer {
|
|
|
109
123
|
this.initOnlineTracking();
|
|
110
124
|
this.offlineCheckTimer = setInterval(() => this.checkOfflineUsers(), HubServer.OFFLINE_CHECK_INTERVAL_MS);
|
|
111
125
|
|
|
112
|
-
return `http://127.0.0.1:${
|
|
126
|
+
return `http://127.0.0.1:${hubPort}`;
|
|
113
127
|
}
|
|
114
128
|
|
|
115
129
|
async stop(): Promise<void> {
|
|
116
130
|
if (this.offlineCheckTimer) { clearInterval(this.offlineCheckTimer); this.offlineCheckTimer = undefined; }
|
|
117
131
|
if (!this.server) return;
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const activeUsers = this.opts.store.listHubUsers("active");
|
|
135
|
+
const ownerId = this.authState.bootstrapAdminUserId || "";
|
|
136
|
+
for (const u of activeUsers) {
|
|
137
|
+
if (u.id === ownerId) continue;
|
|
138
|
+
try {
|
|
139
|
+
this.opts.store.insertHubNotification({
|
|
140
|
+
id: randomUUID(), userId: u.id, type: "hub_shutdown",
|
|
141
|
+
resource: "system", title: `Team server "${this.teamName}" has been shut down by the admin.`,
|
|
142
|
+
});
|
|
143
|
+
} catch { /* best-effort */ }
|
|
144
|
+
}
|
|
145
|
+
} catch { /* best-effort */ }
|
|
146
|
+
|
|
118
147
|
const server = this.server;
|
|
119
148
|
this.server = undefined;
|
|
120
149
|
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
121
150
|
}
|
|
122
151
|
|
|
123
152
|
private get port(): number {
|
|
124
|
-
|
|
153
|
+
const configured = this.opts.config.sharing?.hub?.port;
|
|
154
|
+
const derived = this.opts.defaultHubPort;
|
|
155
|
+
if (derived && (!configured || configured === 18800)) return derived;
|
|
156
|
+
return configured ?? 18800;
|
|
125
157
|
}
|
|
126
158
|
|
|
127
159
|
private get teamName(): string {
|
|
@@ -169,6 +201,20 @@ export class HubServer {
|
|
|
169
201
|
});
|
|
170
202
|
}
|
|
171
203
|
|
|
204
|
+
private embedSkillAsync(skillId: string, name: string, description: string, sourceUserId: string, sourceSkillId: string): void {
|
|
205
|
+
const embedder = this.opts.embedder;
|
|
206
|
+
if (!embedder) return;
|
|
207
|
+
const text = `${name}: ${description}`;
|
|
208
|
+
embedder.embed([text]).then((vectors) => {
|
|
209
|
+
if (vectors[0]) {
|
|
210
|
+
this.opts.store.upsertHubSkillEmbedding(skillId, Array.from(vectors[0]), sourceUserId, sourceSkillId);
|
|
211
|
+
this.opts.log.info(`hub: embedded shared skill ${skillId}`);
|
|
212
|
+
}
|
|
213
|
+
}).catch((err) => {
|
|
214
|
+
this.opts.log.warn(`hub: embedding shared skill failed: ${err}`);
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
172
218
|
private embedMemoryAsync(memoryId: string, summary: string, content: string): void {
|
|
173
219
|
const embedder = this.opts.embedder;
|
|
174
220
|
if (!embedder) return;
|
|
@@ -205,35 +251,70 @@ export class HubServer {
|
|
|
205
251
|
|| (req.headers["x-client-ip"] as string)?.trim()
|
|
206
252
|
|| (req.headers["x-forwarded-for"] as string)?.split(",")[0]?.trim()
|
|
207
253
|
|| req.socket.remoteAddress || "";
|
|
208
|
-
const
|
|
209
|
-
|
|
254
|
+
const identityKey = typeof body.identityKey === "string" ? body.identityKey.trim() : "";
|
|
255
|
+
|
|
256
|
+
let existingUser = identityKey
|
|
257
|
+
? this.userManager.findByIdentityKey(identityKey)
|
|
258
|
+
: null;
|
|
259
|
+
if (!existingUser) {
|
|
260
|
+
const existingUsers = this.opts.store.listHubUsers();
|
|
261
|
+
existingUser = existingUsers.find(u => u.username === username && u.status !== "left" && u.status !== "removed") ?? null;
|
|
262
|
+
}
|
|
263
|
+
|
|
210
264
|
if (existingUser) {
|
|
211
265
|
try { this.opts.store.updateHubUserActivity(existingUser.id, joinIp); } catch { /* best-effort */ }
|
|
266
|
+
|
|
212
267
|
if (existingUser.status === "active") {
|
|
213
268
|
const token = issueUserToken(
|
|
214
269
|
{ userId: existingUser.id, username: existingUser.username, role: existingUser.role, status: "active" },
|
|
215
270
|
this.authSecret,
|
|
216
271
|
);
|
|
217
272
|
this.userManager.approveUser(existingUser.id, token);
|
|
218
|
-
|
|
273
|
+
if (identityKey && !existingUser.identityKey) {
|
|
274
|
+
this.opts.store.upsertHubUser({ ...existingUser, identityKey });
|
|
275
|
+
}
|
|
276
|
+
return this.json(res, 200, { status: "active", userId: existingUser.id, userToken: token, identityKey: existingUser.identityKey || identityKey });
|
|
219
277
|
}
|
|
220
278
|
if (existingUser.status === "pending") {
|
|
221
|
-
|
|
279
|
+
this.notifyAdmins("user_join_request", "user", username, "", { dedup: true });
|
|
280
|
+
return this.json(res, 200, { status: "pending", userId: existingUser.id, identityKey: existingUser.identityKey || identityKey });
|
|
222
281
|
}
|
|
223
282
|
if (existingUser.status === "rejected") {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
283
|
+
if (body.reapply === true) {
|
|
284
|
+
this.userManager.resetToPending(existingUser.id);
|
|
285
|
+
this.notifyAdmins("user_join_request", "user", username, "");
|
|
286
|
+
this.opts.log.info(`Hub: rejected user "${username}" (${existingUser.id}) re-applied, reset to pending`);
|
|
287
|
+
return this.json(res, 200, { status: "pending", userId: existingUser.id, identityKey: existingUser.identityKey || identityKey });
|
|
288
|
+
}
|
|
289
|
+
return this.json(res, 200, { status: "rejected", userId: existingUser.id });
|
|
290
|
+
}
|
|
291
|
+
if (existingUser.status === "removed") {
|
|
292
|
+
this.userManager.rejoinUser(existingUser.id);
|
|
293
|
+
this.notifyAdmins("user_join_request", "user", username, "", { dedup: true });
|
|
294
|
+
this.opts.log.info(`Hub: removed user "${username}" (${existingUser.id}) re-applied via rejoin, reset to pending`);
|
|
295
|
+
return this.json(res, 200, { status: "pending", userId: existingUser.id, identityKey: existingUser.identityKey || identityKey });
|
|
296
|
+
}
|
|
297
|
+
if (existingUser.status === "left") {
|
|
298
|
+
this.userManager.rejoinUser(existingUser.id);
|
|
299
|
+
this.notifyAdmins("user_join_request", "user", username, "", { dedup: true });
|
|
300
|
+
this.opts.log.info(`Hub: left user "${username}" (${existingUser.id}) re-applied via rejoin, reset to pending`);
|
|
301
|
+
return this.json(res, 200, { status: "pending", userId: existingUser.id, identityKey: existingUser.identityKey || identityKey });
|
|
302
|
+
}
|
|
303
|
+
if (existingUser.status === "blocked") {
|
|
304
|
+
return this.json(res, 200, { status: "blocked", userId: existingUser.id });
|
|
227
305
|
}
|
|
228
306
|
}
|
|
307
|
+
|
|
308
|
+
const generatedIdentityKey = identityKey || randomUUID();
|
|
229
309
|
const user = this.userManager.createPendingUser({
|
|
230
310
|
username,
|
|
231
311
|
deviceName: typeof body.deviceName === "string" ? body.deviceName : undefined,
|
|
312
|
+
identityKey: generatedIdentityKey,
|
|
232
313
|
});
|
|
233
314
|
try { this.opts.store.updateHubUserActivity(user.id, joinIp); } catch { /* best-effort */ }
|
|
234
315
|
this.opts.log.info(`Hub: user "${username}" (${user.id}) registered as pending, awaiting admin approval`);
|
|
235
316
|
this.notifyAdmins("user_join_request", "user", username, "");
|
|
236
|
-
return this.json(res, 200, { status: "pending", userId: user.id });
|
|
317
|
+
return this.json(res, 200, { status: "pending", userId: user.id, identityKey: generatedIdentityKey });
|
|
237
318
|
}
|
|
238
319
|
|
|
239
320
|
if (req.method === "POST" && routePath === "/api/v1/hub/registration-status") {
|
|
@@ -251,16 +332,42 @@ export class HubServer {
|
|
|
251
332
|
if (user.status === "rejected") {
|
|
252
333
|
return this.json(res, 200, { status: "rejected" });
|
|
253
334
|
}
|
|
335
|
+
if (user.status === "blocked") {
|
|
336
|
+
return this.json(res, 200, { status: "blocked" });
|
|
337
|
+
}
|
|
338
|
+
if (user.status === "left") {
|
|
339
|
+
return this.json(res, 200, { status: "left" });
|
|
340
|
+
}
|
|
341
|
+
if (user.status === "removed") {
|
|
342
|
+
return this.json(res, 200, { status: "removed" });
|
|
343
|
+
}
|
|
254
344
|
if (user.status === "active") {
|
|
255
345
|
const token = issueUserToken(
|
|
256
346
|
{ userId: user.id, username: user.username, role: user.role, status: user.status },
|
|
257
347
|
this.authSecret,
|
|
258
348
|
);
|
|
349
|
+
this.userManager.approveUser(user.id, token);
|
|
259
350
|
return this.json(res, 200, { status: "active", userToken: token });
|
|
260
351
|
}
|
|
261
352
|
return this.json(res, 200, { status: user.status });
|
|
262
353
|
}
|
|
263
354
|
|
|
355
|
+
if (req.method === "POST" && routePath === "/api/v1/hub/withdraw-pending") {
|
|
356
|
+
const body = await this.readJson(req);
|
|
357
|
+
if (!body || body.teamToken !== this.teamToken) {
|
|
358
|
+
return this.json(res, 403, { error: "invalid_team_token" });
|
|
359
|
+
}
|
|
360
|
+
const userId = String(body.userId || "");
|
|
361
|
+
if (!userId) return this.json(res, 400, { error: "missing_user_id" });
|
|
362
|
+
const user = this.opts.store.getHubUser(userId);
|
|
363
|
+
if (!user) return this.json(res, 200, { ok: true });
|
|
364
|
+
if (user.status === "pending") {
|
|
365
|
+
this.userManager.markUserLeft(userId);
|
|
366
|
+
this.opts.log.info(`Hub: user "${user.username}" (${userId}) withdrew pending application`);
|
|
367
|
+
}
|
|
368
|
+
return this.json(res, 200, { ok: true });
|
|
369
|
+
}
|
|
370
|
+
|
|
264
371
|
// All endpoints below require authentication + rate limiting
|
|
265
372
|
const auth = this.authenticate(req);
|
|
266
373
|
if (!auth) return this.json(res, 401, { error: "unauthorized" });
|
|
@@ -275,12 +382,10 @@ export class HubServer {
|
|
|
275
382
|
}
|
|
276
383
|
|
|
277
384
|
if (req.method === "POST" && routePath === "/api/v1/hub/leave") {
|
|
278
|
-
|
|
279
|
-
this.opts.store.updateHubUserActivity(auth.userId, "", 0);
|
|
280
|
-
} catch { /* best-effort */ }
|
|
385
|
+
this.userManager.markUserLeft(auth.userId);
|
|
281
386
|
this.knownOnlineUsers.delete(auth.userId);
|
|
282
|
-
this.notifyAdmins("
|
|
283
|
-
this.opts.log.info(`Hub: user "${auth.username}" (${auth.userId}) left voluntarily`);
|
|
387
|
+
this.notifyAdmins("user_left", "user", auth.username, auth.userId);
|
|
388
|
+
this.opts.log.info(`Hub: user "${auth.username}" (${auth.userId}) left voluntarily, status set to "left"`);
|
|
284
389
|
return this.json(res, 200, { ok: true });
|
|
285
390
|
}
|
|
286
391
|
|
|
@@ -309,6 +414,10 @@ export class HubServer {
|
|
|
309
414
|
ttlMs,
|
|
310
415
|
);
|
|
311
416
|
this.userManager.approveUser(updated.id, newToken);
|
|
417
|
+
if (updated.id === this.authState.bootstrapAdminUserId) {
|
|
418
|
+
this.authState.bootstrapAdminToken = newToken;
|
|
419
|
+
this.saveAuthState();
|
|
420
|
+
}
|
|
312
421
|
this.opts.log.info(`Hub: user "${auth.userId}" renamed to "${newUsername}"`);
|
|
313
422
|
return this.json(res, 200, { ok: true, username: newUsername, userToken: newToken });
|
|
314
423
|
}
|
|
@@ -321,18 +430,33 @@ export class HubServer {
|
|
|
321
430
|
if (req.method === "POST" && routePath === "/api/v1/hub/admin/approve-user") {
|
|
322
431
|
if (auth.role !== "admin") return this.json(res, 403, { error: "forbidden" });
|
|
323
432
|
const body = await this.readJson(req);
|
|
324
|
-
const
|
|
325
|
-
const
|
|
433
|
+
const userId = String(body.userId);
|
|
434
|
+
const username = String(body.username || "");
|
|
435
|
+
const token = issueUserToken({ userId, username, role: "member", status: "active" }, this.authSecret);
|
|
436
|
+
const approved = this.userManager.approveUser(userId, token);
|
|
326
437
|
if (!approved) return this.json(res, 404, { error: "not_found" });
|
|
327
|
-
try { this.opts.store.updateHubUserActivity(
|
|
438
|
+
try { this.opts.store.updateHubUserActivity(userId, ""); } catch { /* best-effort */ }
|
|
439
|
+
try {
|
|
440
|
+
this.opts.store.insertHubNotification({
|
|
441
|
+
id: randomUUID(), userId, type: "membership_approved",
|
|
442
|
+
resource: "user", title: `Your request to join team "${this.teamName}" has been approved. Welcome!`,
|
|
443
|
+
});
|
|
444
|
+
} catch { /* best-effort */ }
|
|
328
445
|
return this.json(res, 200, { status: "active", token });
|
|
329
446
|
}
|
|
330
447
|
|
|
331
448
|
if (req.method === "POST" && routePath === "/api/v1/hub/admin/reject-user") {
|
|
332
449
|
if (auth.role !== "admin") return this.json(res, 403, { error: "forbidden" });
|
|
333
450
|
const body = await this.readJson(req);
|
|
334
|
-
const
|
|
451
|
+
const userId = String(body.userId);
|
|
452
|
+
const rejected = this.userManager.rejectUser(userId);
|
|
335
453
|
if (!rejected) return this.json(res, 404, { error: "not_found" });
|
|
454
|
+
try {
|
|
455
|
+
this.opts.store.insertHubNotification({
|
|
456
|
+
id: randomUUID(), userId, type: "membership_rejected",
|
|
457
|
+
resource: "user", title: `Your request to join team "${this.teamName}" has been declined.`,
|
|
458
|
+
});
|
|
459
|
+
} catch { /* best-effort */ }
|
|
336
460
|
return this.json(res, 200, { status: "rejected" });
|
|
337
461
|
}
|
|
338
462
|
|
|
@@ -369,6 +493,13 @@ export class HubServer {
|
|
|
369
493
|
const updatedUser = { ...user, role: newRole as "admin" | "member" };
|
|
370
494
|
this.opts.store.upsertHubUser(updatedUser);
|
|
371
495
|
this.opts.log.info(`Hub: admin "${auth.userId}" changed role of "${userId}" to "${newRole}"`);
|
|
496
|
+
try {
|
|
497
|
+
const notifType = newRole === "admin" ? "role_promoted" : "role_demoted";
|
|
498
|
+
this.opts.store.insertHubNotification({
|
|
499
|
+
id: randomUUID(), userId, type: notifType,
|
|
500
|
+
resource: "user", title: `Your role in team "${this.teamName}" has been changed to ${newRole}.`,
|
|
501
|
+
});
|
|
502
|
+
} catch { /* best-effort */ }
|
|
372
503
|
return this.json(res, 200, { ok: true, role: newRole });
|
|
373
504
|
}
|
|
374
505
|
|
|
@@ -395,6 +526,10 @@ export class HubServer {
|
|
|
395
526
|
const updated = this.opts.store.getHubUser(userId)!;
|
|
396
527
|
const finalUser = { ...updated, username: newUsername };
|
|
397
528
|
this.opts.store.upsertHubUser(finalUser);
|
|
529
|
+
if (userId === this.authState.bootstrapAdminUserId) {
|
|
530
|
+
this.authState.bootstrapAdminToken = newToken;
|
|
531
|
+
this.saveAuthState();
|
|
532
|
+
}
|
|
398
533
|
this.opts.log.info(`Hub: admin "${auth.userId}" renamed user "${userId}" to "${newUsername}"`);
|
|
399
534
|
return this.json(res, 200, { ok: true, username: newUsername });
|
|
400
535
|
}
|
|
@@ -406,9 +541,16 @@ export class HubServer {
|
|
|
406
541
|
if (!userId) return this.json(res, 400, { error: "missing_user_id" });
|
|
407
542
|
if (userId === auth.userId) return this.json(res, 400, { error: "cannot_remove_self" });
|
|
408
543
|
if (userId === this.authState.bootstrapAdminUserId) return this.json(res, 403, { error: "cannot_remove_owner", message: "The hub owner cannot be removed" });
|
|
544
|
+
try {
|
|
545
|
+
this.opts.store.insertHubNotification({
|
|
546
|
+
id: randomUUID(), userId, type: "membership_removed",
|
|
547
|
+
resource: "user", title: `You have been removed from team "${this.teamName}" by the admin.`,
|
|
548
|
+
});
|
|
549
|
+
} catch { /* best-effort */ }
|
|
409
550
|
const cleanResources = body?.cleanResources === true;
|
|
410
551
|
const deleted = this.opts.store.deleteHubUser(userId, cleanResources);
|
|
411
552
|
if (!deleted) return this.json(res, 404, { error: "not_found" });
|
|
553
|
+
this.knownOnlineUsers.delete(userId);
|
|
412
554
|
this.opts.log.info(`Hub: admin "${auth.userId}" removed user "${userId}" (cleanResources=${cleanResources})`);
|
|
413
555
|
return this.json(res, 200, { ok: true });
|
|
414
556
|
}
|
|
@@ -598,19 +740,70 @@ export class HubServer {
|
|
|
598
740
|
}
|
|
599
741
|
|
|
600
742
|
if (req.method === "GET" && routePath === "/api/v1/hub/skills") {
|
|
601
|
-
const
|
|
743
|
+
const skillQuery = String(url.searchParams.get("query") || "");
|
|
744
|
+
const skillMaxResults = Number(url.searchParams.get("maxResults") || 10);
|
|
745
|
+
const ftsSkillHits = this.opts.store.searchHubSkills(skillQuery, {
|
|
602
746
|
userId: auth.userId,
|
|
603
|
-
maxResults:
|
|
604
|
-
})
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
747
|
+
maxResults: skillMaxResults * 2,
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
let mergedSkillIds: string[];
|
|
751
|
+
if (this.opts.embedder && skillQuery) {
|
|
752
|
+
try {
|
|
753
|
+
const [queryVec] = await this.opts.embedder.embed([skillQuery]);
|
|
754
|
+
if (queryVec) {
|
|
755
|
+
const skillEmbs = this.opts.store.getVisibleHubSkillEmbeddings();
|
|
756
|
+
const cosineSim = (vec: Float32Array) => {
|
|
757
|
+
let dot = 0, nA = 0, nB = 0;
|
|
758
|
+
for (let i = 0; i < queryVec.length && i < vec.length; i++) {
|
|
759
|
+
dot += queryVec[i] * vec[i]; nA += queryVec[i] * queryVec[i]; nB += vec[i] * vec[i];
|
|
760
|
+
}
|
|
761
|
+
return nA > 0 && nB > 0 ? dot / (Math.sqrt(nA) * Math.sqrt(nB)) : 0;
|
|
762
|
+
};
|
|
763
|
+
const vecScored = skillEmbs
|
|
764
|
+
.map(e => ({ id: e.skillId, score: cosineSim(e.vector) }))
|
|
765
|
+
.filter(e => e.score > 0.3)
|
|
766
|
+
.sort((a, b) => b.score - a.score)
|
|
767
|
+
.slice(0, skillMaxResults * 2);
|
|
768
|
+
|
|
769
|
+
const K = 60;
|
|
770
|
+
const rrfScores = new Map<string, number>();
|
|
771
|
+
ftsSkillHits.forEach(({ hit }, idx) => {
|
|
772
|
+
rrfScores.set(hit.id, (rrfScores.get(hit.id) ?? 0) + 1 / (K + idx + 1));
|
|
773
|
+
});
|
|
774
|
+
vecScored.forEach(({ id }, idx) => {
|
|
775
|
+
rrfScores.set(id, (rrfScores.get(id) ?? 0) + 1 / (K + idx + 1));
|
|
776
|
+
});
|
|
777
|
+
mergedSkillIds = [...rrfScores.entries()].sort((a, b) => b[1] - a[1]).slice(0, skillMaxResults).map(([id]) => id);
|
|
778
|
+
} else {
|
|
779
|
+
mergedSkillIds = ftsSkillHits.slice(0, skillMaxResults).map(({ hit }) => hit.id);
|
|
780
|
+
}
|
|
781
|
+
} catch {
|
|
782
|
+
mergedSkillIds = ftsSkillHits.slice(0, skillMaxResults).map(({ hit }) => hit.id);
|
|
783
|
+
}
|
|
784
|
+
} else {
|
|
785
|
+
mergedSkillIds = ftsSkillHits.slice(0, skillMaxResults).map(({ hit }) => hit.id);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const ftsSkillMap = new Map(ftsSkillHits.map(({ hit }) => [hit.id, hit]));
|
|
789
|
+
const hits = mergedSkillIds.map(id => {
|
|
790
|
+
const hit = ftsSkillMap.get(id);
|
|
791
|
+
if (hit) {
|
|
792
|
+
return {
|
|
793
|
+
skillId: hit.id, name: hit.name, description: hit.description,
|
|
794
|
+
version: hit.version, visibility: hit.visibility, groupName: hit.group_name,
|
|
795
|
+
ownerName: hit.owner_name || "unknown", ownerStatus: hit.owner_status || "",
|
|
796
|
+
qualityScore: hit.quality_score,
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
const skill = this.opts.store.getHubSkillById(id);
|
|
800
|
+
if (!skill) return null;
|
|
801
|
+
return {
|
|
802
|
+
skillId: skill.id, name: skill.name, description: skill.description,
|
|
803
|
+
version: skill.version, visibility: skill.visibility, groupName: "",
|
|
804
|
+
ownerName: "unknown", ownerStatus: "", qualityScore: skill.qualityScore,
|
|
805
|
+
};
|
|
806
|
+
}).filter(Boolean);
|
|
614
807
|
return this.json(res, 200, { hits });
|
|
615
808
|
}
|
|
616
809
|
|
|
@@ -636,6 +829,7 @@ export class HubServer {
|
|
|
636
829
|
createdAt: existing?.createdAt ?? Date.now(),
|
|
637
830
|
updatedAt: Date.now(),
|
|
638
831
|
});
|
|
832
|
+
this.embedSkillAsync(skillId, String(metadata.name || sourceSkillId), String(metadata.description || ""), auth.userId, sourceSkillId);
|
|
639
833
|
if (!existing) {
|
|
640
834
|
this.notifyAdmins("resource_shared", "skill", String(metadata.name || sourceSkillId), auth.userId);
|
|
641
835
|
}
|
|
@@ -756,7 +950,18 @@ export class HubServer {
|
|
|
756
950
|
const deleted = this.opts.store.deleteHubMemoryById(memoryId);
|
|
757
951
|
if (!deleted) return this.json(res, 404, { error: "not_found" });
|
|
758
952
|
if (memInfo) {
|
|
759
|
-
|
|
953
|
+
const payload = JSON.stringify({
|
|
954
|
+
memoryId,
|
|
955
|
+
sourceChunkId: memInfo.sourceChunkId,
|
|
956
|
+
});
|
|
957
|
+
this.opts.store.insertHubNotification({
|
|
958
|
+
id: randomUUID(),
|
|
959
|
+
userId: memInfo.sourceUserId,
|
|
960
|
+
type: "resource_removed",
|
|
961
|
+
resource: "memory",
|
|
962
|
+
title: memInfo.summary || memInfo.id,
|
|
963
|
+
message: payload,
|
|
964
|
+
});
|
|
760
965
|
}
|
|
761
966
|
return this.json(res, 200, { ok: true });
|
|
762
967
|
}
|
|
@@ -802,10 +1007,13 @@ export class HubServer {
|
|
|
802
1007
|
return this.json(res, 404, { error: "not_found" });
|
|
803
1008
|
}
|
|
804
1009
|
|
|
805
|
-
private notifyAdmins(type: string, resource: string, title: string, fromUserId: string): void {
|
|
1010
|
+
private notifyAdmins(type: string, resource: string, title: string, fromUserId: string, opts?: { dedup?: boolean; deduoWindowMs?: number }): void {
|
|
806
1011
|
try {
|
|
807
1012
|
const admins = this.opts.store.listHubUsers("active").filter(u => u.role === "admin" && u.id !== fromUserId);
|
|
808
1013
|
for (const admin of admins) {
|
|
1014
|
+
if (opts?.dedup && this.opts.store.hasRecentHubNotification(admin.id, type, resource, opts.deduoWindowMs ?? 300_000)) {
|
|
1015
|
+
continue;
|
|
1016
|
+
}
|
|
809
1017
|
this.opts.store.insertHubNotification({ id: randomUUID(), userId: admin.id, type, resource, title });
|
|
810
1018
|
}
|
|
811
1019
|
} catch { /* best-effort */ }
|
package/src/hub/user-manager.ts
CHANGED
|
@@ -4,13 +4,24 @@ 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 & {
|
|
7
|
+
type ManagedHubUser = UserInfo & {
|
|
8
|
+
tokenHash: string;
|
|
9
|
+
createdAt: number;
|
|
10
|
+
approvedAt: number | null;
|
|
11
|
+
lastIp: string;
|
|
12
|
+
lastActiveAt: number | null;
|
|
13
|
+
identityKey?: string;
|
|
14
|
+
leftAt?: number | null;
|
|
15
|
+
removedAt?: number | null;
|
|
16
|
+
rejectedAt?: number | null;
|
|
17
|
+
rejoinRequestedAt?: number | null;
|
|
18
|
+
};
|
|
8
19
|
|
|
9
20
|
export class HubUserManager {
|
|
10
21
|
constructor(private store: SqliteStore, private log: Logger) {}
|
|
11
22
|
|
|
12
|
-
createPendingUser(input: { username: string; deviceName?: string }): ManagedHubUser {
|
|
13
|
-
const user = {
|
|
23
|
+
createPendingUser(input: { username: string; deviceName?: string; identityKey?: string }): ManagedHubUser {
|
|
24
|
+
const user: ManagedHubUser = {
|
|
14
25
|
id: randomUUID(),
|
|
15
26
|
username: input.username,
|
|
16
27
|
deviceName: input.deviceName,
|
|
@@ -22,11 +33,36 @@ export class HubUserManager {
|
|
|
22
33
|
approvedAt: null,
|
|
23
34
|
lastIp: "",
|
|
24
35
|
lastActiveAt: null,
|
|
36
|
+
identityKey: input.identityKey || "",
|
|
25
37
|
};
|
|
26
38
|
this.store.upsertHubUser(user);
|
|
27
39
|
return user;
|
|
28
40
|
}
|
|
29
41
|
|
|
42
|
+
findByIdentityKey(identityKey: string): ManagedHubUser | null {
|
|
43
|
+
if (!identityKey) return null;
|
|
44
|
+
return this.store.findHubUserByIdentityKey(identityKey);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
markUserLeft(userId: string): boolean {
|
|
48
|
+
this.log.info(`Hub: user "${userId}" marked as left`);
|
|
49
|
+
return this.store.markHubUserLeft(userId);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
rejoinUser(userId: string): ManagedHubUser | null {
|
|
53
|
+
const user = this.store.getHubUser(userId);
|
|
54
|
+
if (!user) return null;
|
|
55
|
+
const updated: ManagedHubUser = {
|
|
56
|
+
...user,
|
|
57
|
+
status: "pending" as const,
|
|
58
|
+
tokenHash: "",
|
|
59
|
+
rejoinRequestedAt: Date.now(),
|
|
60
|
+
};
|
|
61
|
+
this.store.upsertHubUser(updated);
|
|
62
|
+
this.log.info(`Hub: user "${userId}" (${user.username}) requested rejoin, previous status: ${user.status}`);
|
|
63
|
+
return updated;
|
|
64
|
+
}
|
|
65
|
+
|
|
30
66
|
listPendingUsers(): ManagedHubUser[] {
|
|
31
67
|
return this.store.listHubUsers("pending");
|
|
32
68
|
}
|
|
@@ -105,7 +141,7 @@ export class HubUserManager {
|
|
|
105
141
|
|
|
106
142
|
isUsernameTaken(username: string, excludeUserId?: string): boolean {
|
|
107
143
|
const users = this.store.listHubUsers();
|
|
108
|
-
return users.some(u => u.username === username && u.id !== excludeUserId);
|
|
144
|
+
return users.some(u => u.username === username && u.id !== excludeUserId && u.status !== "left" && u.status !== "removed");
|
|
109
145
|
}
|
|
110
146
|
|
|
111
147
|
updateUsername(userId: string, newUsername: string): ManagedHubUser | null {
|
|
@@ -119,10 +155,10 @@ export class HubUserManager {
|
|
|
119
155
|
rejectUser(userId: string): ManagedHubUser | null {
|
|
120
156
|
const user = this.store.getHubUser(userId);
|
|
121
157
|
if (!user) return null;
|
|
122
|
-
const updated = {
|
|
158
|
+
const updated: ManagedHubUser = {
|
|
123
159
|
...user,
|
|
124
160
|
status: "rejected" as const,
|
|
125
|
-
|
|
161
|
+
rejectedAt: Date.now(),
|
|
126
162
|
};
|
|
127
163
|
this.store.upsertHubUser(updated);
|
|
128
164
|
return updated;
|
package/src/ingest/chunker.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
export type ChunkKind = "paragraph" | "code_block" | "error_stack" | "list" | "command";
|
|
2
|
+
|
|
1
3
|
export interface RawChunk {
|
|
2
4
|
content: string;
|
|
3
|
-
kind:
|
|
5
|
+
kind: ChunkKind;
|
|
4
6
|
}
|
|
5
7
|
|
|
6
8
|
const MAX_CHUNK_CHARS = 3000;
|
|
@@ -28,21 +30,25 @@ const COMMAND_LINE_RE = /^(?:\$|>|#)\s+.+$/gm;
|
|
|
28
30
|
*/
|
|
29
31
|
export function chunkText(text: string): RawChunk[] {
|
|
30
32
|
let remaining = text;
|
|
31
|
-
const slots: Array<{ placeholder: string; content: string }> = [];
|
|
33
|
+
const slots: Array<{ placeholder: string; content: string; kind: ChunkKind }> = [];
|
|
32
34
|
let counter = 0;
|
|
33
35
|
|
|
34
|
-
function ph(content: string): string {
|
|
36
|
+
function ph(content: string, kind: ChunkKind = "paragraph"): string {
|
|
35
37
|
const tag = `\x00SLOT_${counter++}\x00`;
|
|
36
|
-
slots.push({ placeholder: tag, content: content.trim() });
|
|
38
|
+
slots.push({ placeholder: tag, content: content.trim(), kind });
|
|
37
39
|
return tag;
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
remaining = remaining.replace(FENCED_CODE_RE, (m) => ph(m));
|
|
42
|
+
remaining = remaining.replace(FENCED_CODE_RE, (m) => ph(m, "code_block"));
|
|
41
43
|
remaining = extractBraceBlocks(remaining, ph);
|
|
42
44
|
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
const structuralKinds: Array<[RegExp, ChunkKind]> = [
|
|
46
|
+
[ERROR_STACK_RE, "error_stack"],
|
|
47
|
+
[LIST_BLOCK_RE, "list"],
|
|
48
|
+
[COMMAND_LINE_RE, "command"],
|
|
49
|
+
];
|
|
50
|
+
for (const [re, kind] of structuralKinds) {
|
|
51
|
+
remaining = remaining.replace(re, (m) => ph(m, kind));
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
const raw: RawChunk[] = [];
|
|
@@ -57,7 +63,7 @@ export function chunkText(text: string): RawChunk[] {
|
|
|
57
63
|
for (const part of parts) {
|
|
58
64
|
const slot = slots.find((s) => s.placeholder === part);
|
|
59
65
|
if (slot) {
|
|
60
|
-
raw.push({ content: slot.content, kind:
|
|
66
|
+
raw.push({ content: slot.content, kind: slot.kind });
|
|
61
67
|
} else if (part.trim().length >= MIN_CHUNK_CHARS) {
|
|
62
68
|
raw.push({ content: part.trim(), kind: "paragraph" });
|
|
63
69
|
}
|
|
@@ -69,7 +75,7 @@ export function chunkText(text: string): RawChunk[] {
|
|
|
69
75
|
|
|
70
76
|
for (const s of slots) {
|
|
71
77
|
if (!raw.some((c) => c.content === s.content)) {
|
|
72
|
-
raw.push({ content: s.content, kind:
|
|
78
|
+
raw.push({ content: s.content, kind: s.kind });
|
|
73
79
|
}
|
|
74
80
|
}
|
|
75
81
|
|
|
@@ -85,7 +91,7 @@ export function chunkText(text: string): RawChunk[] {
|
|
|
85
91
|
*/
|
|
86
92
|
function extractBraceBlocks(
|
|
87
93
|
text: string,
|
|
88
|
-
ph: (content: string) => string,
|
|
94
|
+
ph: (content: string, kind?: ChunkKind) => string,
|
|
89
95
|
): string {
|
|
90
96
|
const lines = text.split("\n");
|
|
91
97
|
const result: string[] = [];
|
|
@@ -119,7 +125,7 @@ function extractBraceBlocks(
|
|
|
119
125
|
if (depth <= 0 || (BLOCK_CLOSE_RE.test(line) && depth <= 0)) {
|
|
120
126
|
const block = blockLines.join("\n");
|
|
121
127
|
if (block.trim().length >= MIN_CHUNK_CHARS) {
|
|
122
|
-
result.push(ph(block));
|
|
128
|
+
result.push(ph(block, "code_block"));
|
|
123
129
|
} else {
|
|
124
130
|
result.push(block);
|
|
125
131
|
}
|
|
@@ -135,7 +141,7 @@ function extractBraceBlocks(
|
|
|
135
141
|
if (blockLines.length > 0) {
|
|
136
142
|
const block = blockLines.join("\n");
|
|
137
143
|
if (block.trim().length >= MIN_CHUNK_CHARS) {
|
|
138
|
-
result.push(ph(block));
|
|
144
|
+
result.push(ph(block, "code_block"));
|
|
139
145
|
} else {
|
|
140
146
|
result.push(block);
|
|
141
147
|
}
|
|
@@ -49,8 +49,8 @@ function normalizeEndpointForProvider(
|
|
|
49
49
|
function loadOpenClawFallbackConfig(log: Logger): SummarizerConfig | undefined {
|
|
50
50
|
try {
|
|
51
51
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
52
|
-
const
|
|
53
|
-
|
|
52
|
+
const cfgPath = process.env.OPENCLAW_CONFIG_PATH
|
|
53
|
+
|| path.join(process.env.OPENCLAW_STATE_DIR || path.join(home, ".openclaw"), "openclaw.json");
|
|
54
54
|
if (!fs.existsSync(cfgPath)) return undefined;
|
|
55
55
|
|
|
56
56
|
const raw = JSON.parse(fs.readFileSync(cfgPath, "utf-8"));
|