@memtensor/memos-local-openclaw-plugin 1.0.4-beta.9 → 1.0.5
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 +89 -8
- 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 +240 -35
- 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 +22 -4
- 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 +57 -0
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +290 -35
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/telemetry.d.ts +4 -1
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +39 -12
- 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 +564 -225
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +9 -0
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +357 -108
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +412 -53
- 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 +92 -8
- package/src/config.ts +2 -1
- package/src/hub/server.ts +235 -35
- 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 +20 -4
- 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 +318 -40
- package/src/telemetry.ts +39 -14
- package/src/types.ts +11 -0
- package/src/viewer/html.ts +564 -225
- package/src/viewer/server.ts +333 -105
- 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,40 +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 });
|
|
222
|
-
return this.json(res, 200, { status: "pending", userId: existingUser.id });
|
|
280
|
+
return this.json(res, 200, { status: "pending", userId: existingUser.id, identityKey: existingUser.identityKey || identityKey });
|
|
223
281
|
}
|
|
224
282
|
if (existingUser.status === "rejected") {
|
|
225
283
|
if (body.reapply === true) {
|
|
226
284
|
this.userManager.resetToPending(existingUser.id);
|
|
227
285
|
this.notifyAdmins("user_join_request", "user", username, "");
|
|
228
286
|
this.opts.log.info(`Hub: rejected user "${username}" (${existingUser.id}) re-applied, reset to pending`);
|
|
229
|
-
return this.json(res, 200, { status: "pending", userId: existingUser.id });
|
|
287
|
+
return this.json(res, 200, { status: "pending", userId: existingUser.id, identityKey: existingUser.identityKey || identityKey });
|
|
230
288
|
}
|
|
231
289
|
return this.json(res, 200, { status: "rejected", userId: existingUser.id });
|
|
232
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 });
|
|
305
|
+
}
|
|
233
306
|
}
|
|
307
|
+
|
|
308
|
+
const generatedIdentityKey = identityKey || randomUUID();
|
|
234
309
|
const user = this.userManager.createPendingUser({
|
|
235
310
|
username,
|
|
236
311
|
deviceName: typeof body.deviceName === "string" ? body.deviceName : undefined,
|
|
312
|
+
identityKey: generatedIdentityKey,
|
|
237
313
|
});
|
|
238
314
|
try { this.opts.store.updateHubUserActivity(user.id, joinIp); } catch { /* best-effort */ }
|
|
239
315
|
this.opts.log.info(`Hub: user "${username}" (${user.id}) registered as pending, awaiting admin approval`);
|
|
240
316
|
this.notifyAdmins("user_join_request", "user", username, "");
|
|
241
|
-
return this.json(res, 200, { status: "pending", userId: user.id });
|
|
317
|
+
return this.json(res, 200, { status: "pending", userId: user.id, identityKey: generatedIdentityKey });
|
|
242
318
|
}
|
|
243
319
|
|
|
244
320
|
if (req.method === "POST" && routePath === "/api/v1/hub/registration-status") {
|
|
@@ -256,16 +332,42 @@ export class HubServer {
|
|
|
256
332
|
if (user.status === "rejected") {
|
|
257
333
|
return this.json(res, 200, { status: "rejected" });
|
|
258
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
|
+
}
|
|
259
344
|
if (user.status === "active") {
|
|
260
345
|
const token = issueUserToken(
|
|
261
346
|
{ userId: user.id, username: user.username, role: user.role, status: user.status },
|
|
262
347
|
this.authSecret,
|
|
263
348
|
);
|
|
349
|
+
this.userManager.approveUser(user.id, token);
|
|
264
350
|
return this.json(res, 200, { status: "active", userToken: token });
|
|
265
351
|
}
|
|
266
352
|
return this.json(res, 200, { status: user.status });
|
|
267
353
|
}
|
|
268
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
|
+
|
|
269
371
|
// All endpoints below require authentication + rate limiting
|
|
270
372
|
const auth = this.authenticate(req);
|
|
271
373
|
if (!auth) return this.json(res, 401, { error: "unauthorized" });
|
|
@@ -280,12 +382,10 @@ export class HubServer {
|
|
|
280
382
|
}
|
|
281
383
|
|
|
282
384
|
if (req.method === "POST" && routePath === "/api/v1/hub/leave") {
|
|
283
|
-
|
|
284
|
-
this.opts.store.updateHubUserActivity(auth.userId, "", 0);
|
|
285
|
-
} catch { /* best-effort */ }
|
|
385
|
+
this.userManager.markUserLeft(auth.userId);
|
|
286
386
|
this.knownOnlineUsers.delete(auth.userId);
|
|
287
|
-
this.notifyAdmins("
|
|
288
|
-
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"`);
|
|
289
389
|
return this.json(res, 200, { ok: true });
|
|
290
390
|
}
|
|
291
391
|
|
|
@@ -314,6 +414,10 @@ export class HubServer {
|
|
|
314
414
|
ttlMs,
|
|
315
415
|
);
|
|
316
416
|
this.userManager.approveUser(updated.id, newToken);
|
|
417
|
+
if (updated.id === this.authState.bootstrapAdminUserId) {
|
|
418
|
+
this.authState.bootstrapAdminToken = newToken;
|
|
419
|
+
this.saveAuthState();
|
|
420
|
+
}
|
|
317
421
|
this.opts.log.info(`Hub: user "${auth.userId}" renamed to "${newUsername}"`);
|
|
318
422
|
return this.json(res, 200, { ok: true, username: newUsername, userToken: newToken });
|
|
319
423
|
}
|
|
@@ -326,18 +430,33 @@ export class HubServer {
|
|
|
326
430
|
if (req.method === "POST" && routePath === "/api/v1/hub/admin/approve-user") {
|
|
327
431
|
if (auth.role !== "admin") return this.json(res, 403, { error: "forbidden" });
|
|
328
432
|
const body = await this.readJson(req);
|
|
329
|
-
const
|
|
330
|
-
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);
|
|
331
437
|
if (!approved) return this.json(res, 404, { error: "not_found" });
|
|
332
|
-
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 */ }
|
|
333
445
|
return this.json(res, 200, { status: "active", token });
|
|
334
446
|
}
|
|
335
447
|
|
|
336
448
|
if (req.method === "POST" && routePath === "/api/v1/hub/admin/reject-user") {
|
|
337
449
|
if (auth.role !== "admin") return this.json(res, 403, { error: "forbidden" });
|
|
338
450
|
const body = await this.readJson(req);
|
|
339
|
-
const
|
|
451
|
+
const userId = String(body.userId);
|
|
452
|
+
const rejected = this.userManager.rejectUser(userId);
|
|
340
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 */ }
|
|
341
460
|
return this.json(res, 200, { status: "rejected" });
|
|
342
461
|
}
|
|
343
462
|
|
|
@@ -374,6 +493,13 @@ export class HubServer {
|
|
|
374
493
|
const updatedUser = { ...user, role: newRole as "admin" | "member" };
|
|
375
494
|
this.opts.store.upsertHubUser(updatedUser);
|
|
376
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 */ }
|
|
377
503
|
return this.json(res, 200, { ok: true, role: newRole });
|
|
378
504
|
}
|
|
379
505
|
|
|
@@ -400,6 +526,10 @@ export class HubServer {
|
|
|
400
526
|
const updated = this.opts.store.getHubUser(userId)!;
|
|
401
527
|
const finalUser = { ...updated, username: newUsername };
|
|
402
528
|
this.opts.store.upsertHubUser(finalUser);
|
|
529
|
+
if (userId === this.authState.bootstrapAdminUserId) {
|
|
530
|
+
this.authState.bootstrapAdminToken = newToken;
|
|
531
|
+
this.saveAuthState();
|
|
532
|
+
}
|
|
403
533
|
this.opts.log.info(`Hub: admin "${auth.userId}" renamed user "${userId}" to "${newUsername}"`);
|
|
404
534
|
return this.json(res, 200, { ok: true, username: newUsername });
|
|
405
535
|
}
|
|
@@ -411,9 +541,16 @@ export class HubServer {
|
|
|
411
541
|
if (!userId) return this.json(res, 400, { error: "missing_user_id" });
|
|
412
542
|
if (userId === auth.userId) return this.json(res, 400, { error: "cannot_remove_self" });
|
|
413
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 */ }
|
|
414
550
|
const cleanResources = body?.cleanResources === true;
|
|
415
551
|
const deleted = this.opts.store.deleteHubUser(userId, cleanResources);
|
|
416
552
|
if (!deleted) return this.json(res, 404, { error: "not_found" });
|
|
553
|
+
this.knownOnlineUsers.delete(userId);
|
|
417
554
|
this.opts.log.info(`Hub: admin "${auth.userId}" removed user "${userId}" (cleanResources=${cleanResources})`);
|
|
418
555
|
return this.json(res, 200, { ok: true });
|
|
419
556
|
}
|
|
@@ -603,19 +740,70 @@ export class HubServer {
|
|
|
603
740
|
}
|
|
604
741
|
|
|
605
742
|
if (req.method === "GET" && routePath === "/api/v1/hub/skills") {
|
|
606
|
-
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, {
|
|
607
746
|
userId: auth.userId,
|
|
608
|
-
maxResults:
|
|
609
|
-
})
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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);
|
|
619
807
|
return this.json(res, 200, { hits });
|
|
620
808
|
}
|
|
621
809
|
|
|
@@ -641,6 +829,7 @@ export class HubServer {
|
|
|
641
829
|
createdAt: existing?.createdAt ?? Date.now(),
|
|
642
830
|
updatedAt: Date.now(),
|
|
643
831
|
});
|
|
832
|
+
this.embedSkillAsync(skillId, String(metadata.name || sourceSkillId), String(metadata.description || ""), auth.userId, sourceSkillId);
|
|
644
833
|
if (!existing) {
|
|
645
834
|
this.notifyAdmins("resource_shared", "skill", String(metadata.name || sourceSkillId), auth.userId);
|
|
646
835
|
}
|
|
@@ -761,7 +950,18 @@ export class HubServer {
|
|
|
761
950
|
const deleted = this.opts.store.deleteHubMemoryById(memoryId);
|
|
762
951
|
if (!deleted) return this.json(res, 404, { error: "not_found" });
|
|
763
952
|
if (memInfo) {
|
|
764
|
-
|
|
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
|
+
});
|
|
765
965
|
}
|
|
766
966
|
return this.json(res, 200, { ok: true });
|
|
767
967
|
}
|
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"));
|