@memtensor/memos-local-openclaw-plugin 1.0.4-beta.9 → 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 +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.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 +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 +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 +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 +27 -9
- 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
|
Binary file
|
package/src/capture/index.ts
CHANGED
|
@@ -167,8 +167,11 @@ export function stripInboundMetadata(text: string): string {
|
|
|
167
167
|
/** Strip <think…>…</think> blocks emitted by DeepSeek-style reasoning models. */
|
|
168
168
|
const THINKING_TAG_RE = /<think[\s>][\s\S]*?<\/think>\s*/gi;
|
|
169
169
|
|
|
170
|
+
/** Unwrap <final>…</final> tags from MiniMax-style models (keep content, strip tags). */
|
|
171
|
+
const FINAL_TAG_RE = /<\/?final\s*>/gi;
|
|
172
|
+
|
|
170
173
|
function stripThinkingTags(text: string): string {
|
|
171
|
-
return text.replace(THINKING_TAG_RE, "");
|
|
174
|
+
return text.replace(THINKING_TAG_RE, "").replace(FINAL_TAG_RE, "").trim();
|
|
172
175
|
}
|
|
173
176
|
|
|
174
177
|
function extractEnvelopeTimestamp(text: string): number | null {
|
package/src/client/connector.ts
CHANGED
|
@@ -10,6 +10,7 @@ export interface HubSessionInfo {
|
|
|
10
10
|
userToken: string;
|
|
11
11
|
role: UserRole;
|
|
12
12
|
connectedAt: number;
|
|
13
|
+
identityKey?: string;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
export interface HubStatusInfo {
|
|
@@ -20,6 +21,7 @@ export interface HubStatusInfo {
|
|
|
20
21
|
username: string;
|
|
21
22
|
role: UserRole;
|
|
22
23
|
status: UserStatus | string;
|
|
24
|
+
groups?: Array<{ id: string; name: string }>;
|
|
23
25
|
};
|
|
24
26
|
}
|
|
25
27
|
|
|
@@ -54,6 +56,8 @@ export async function connectToHub(store: SqliteStore, config: MemosLocalConfig,
|
|
|
54
56
|
userToken: result.userToken,
|
|
55
57
|
role: "member",
|
|
56
58
|
connectedAt: Date.now(),
|
|
59
|
+
identityKey: persisted.identityKey || "",
|
|
60
|
+
lastKnownStatus: "active",
|
|
57
61
|
});
|
|
58
62
|
return store.getClientHubConnection()!;
|
|
59
63
|
}
|
|
@@ -63,6 +67,12 @@ export async function connectToHub(store: SqliteStore, config: MemosLocalConfig,
|
|
|
63
67
|
if (result.status === "rejected") {
|
|
64
68
|
throw new Error("Join request was rejected by the Hub admin.");
|
|
65
69
|
}
|
|
70
|
+
if (result.status === "blocked") {
|
|
71
|
+
throw new Error("Your account has been blocked by the Hub admin.");
|
|
72
|
+
}
|
|
73
|
+
if (result.status === "left" || result.status === "removed") {
|
|
74
|
+
log.info(`User status is "${result.status}", will try to rejoin.`);
|
|
75
|
+
}
|
|
66
76
|
} catch (err) {
|
|
67
77
|
if (err instanceof PendingApprovalError) throw err;
|
|
68
78
|
log.warn(`registration-status check failed, falling back to autoJoinHub: ${err}`);
|
|
@@ -78,6 +88,7 @@ export async function connectToHub(store: SqliteStore, config: MemosLocalConfig,
|
|
|
78
88
|
|
|
79
89
|
const hubUrl = normalizeHubUrl(hubAddress);
|
|
80
90
|
const me = await hubRequestJson(hubUrl, userToken, "/api/v1/hub/me", { method: "GET" }) as any;
|
|
91
|
+
const persisted = store.getClientHubConnection();
|
|
81
92
|
store.setClientHubConnection({
|
|
82
93
|
hubUrl,
|
|
83
94
|
userId: String(me.id),
|
|
@@ -85,6 +96,8 @@ export async function connectToHub(store: SqliteStore, config: MemosLocalConfig,
|
|
|
85
96
|
userToken,
|
|
86
97
|
role: String(me.role ?? "member") as UserRole,
|
|
87
98
|
connectedAt: Date.now(),
|
|
99
|
+
identityKey: persisted?.identityKey || String(me.identityKey ?? ""),
|
|
100
|
+
lastKnownStatus: "active",
|
|
88
101
|
});
|
|
89
102
|
return store.getClientHubConnection()!;
|
|
90
103
|
}
|
|
@@ -95,9 +108,13 @@ export async function getHubStatus(store: SqliteStore, config: MemosLocalConfig)
|
|
|
95
108
|
const hubAddress = conn?.hubUrl || (configHubAddress ? normalizeHubUrl(configHubAddress) : "");
|
|
96
109
|
const userToken = conn?.userToken || config.sharing?.client?.userToken || "";
|
|
97
110
|
|
|
98
|
-
// If DB has a connection to a different Hub than config, the DB data is stale
|
|
99
111
|
if (conn && configHubAddress && conn.hubUrl && normalizeHubUrl(configHubAddress) !== conn.hubUrl) {
|
|
100
|
-
store.
|
|
112
|
+
store.setClientHubConnection({
|
|
113
|
+
...conn,
|
|
114
|
+
hubUrl: normalizeHubUrl(configHubAddress),
|
|
115
|
+
userToken: "",
|
|
116
|
+
lastKnownStatus: "hub_changed",
|
|
117
|
+
});
|
|
101
118
|
return { connected: false, user: null };
|
|
102
119
|
}
|
|
103
120
|
|
|
@@ -129,6 +146,8 @@ export async function getHubStatus(store: SqliteStore, config: MemosLocalConfig)
|
|
|
129
146
|
userToken: result.userToken,
|
|
130
147
|
role: "member",
|
|
131
148
|
connectedAt: Date.now(),
|
|
149
|
+
identityKey: conn.identityKey || "",
|
|
150
|
+
lastKnownStatus: "active",
|
|
132
151
|
});
|
|
133
152
|
const me = await hubRequestJson(normalizeHubUrl(hubAddress), result.userToken, "/api/v1/hub/me", { method: "GET" }) as any;
|
|
134
153
|
return {
|
|
@@ -169,12 +188,10 @@ export async function getHubStatus(store: SqliteStore, config: MemosLocalConfig)
|
|
|
169
188
|
const latestRole = String(me.role ?? "member") as UserRole;
|
|
170
189
|
if (conn && (conn.username !== latestUsername || conn.role !== latestRole)) {
|
|
171
190
|
store.setClientHubConnection({
|
|
172
|
-
|
|
173
|
-
userId: conn.userId,
|
|
191
|
+
...conn,
|
|
174
192
|
username: latestUsername,
|
|
175
|
-
userToken: conn.userToken,
|
|
176
193
|
role: latestRole,
|
|
177
|
-
|
|
194
|
+
lastKnownStatus: "active",
|
|
178
195
|
});
|
|
179
196
|
}
|
|
180
197
|
return {
|
|
@@ -185,9 +202,63 @@ export async function getHubStatus(store: SqliteStore, config: MemosLocalConfig)
|
|
|
185
202
|
username: latestUsername,
|
|
186
203
|
role: latestRole,
|
|
187
204
|
status: String(me.status ?? "active"),
|
|
205
|
+
groups: Array.isArray(me.groups) ? me.groups : [],
|
|
188
206
|
},
|
|
189
207
|
};
|
|
190
|
-
} catch {
|
|
208
|
+
} catch (err: any) {
|
|
209
|
+
const is401 = typeof err?.message === "string" && err.message.includes("(401)");
|
|
210
|
+
if (is401 && conn) {
|
|
211
|
+
const teamToken = config.sharing?.client?.teamToken ?? "";
|
|
212
|
+
if (hubAddress && teamToken) {
|
|
213
|
+
try {
|
|
214
|
+
const regResult = await hubRequestJson(normalizeHubUrl(hubAddress), "", "/api/v1/hub/registration-status", {
|
|
215
|
+
method: "POST",
|
|
216
|
+
body: JSON.stringify({ teamToken, userId: conn.userId }),
|
|
217
|
+
}) as any;
|
|
218
|
+
if (regResult.status === "active" && regResult.userToken) {
|
|
219
|
+
store.setClientHubConnection({
|
|
220
|
+
...conn,
|
|
221
|
+
hubUrl: normalizeHubUrl(hubAddress),
|
|
222
|
+
userToken: regResult.userToken,
|
|
223
|
+
connectedAt: Date.now(),
|
|
224
|
+
lastKnownStatus: "active",
|
|
225
|
+
});
|
|
226
|
+
try {
|
|
227
|
+
const me = await hubRequestJson(normalizeHubUrl(hubAddress), regResult.userToken, "/api/v1/hub/me", { method: "GET" }) as any;
|
|
228
|
+
return {
|
|
229
|
+
connected: true,
|
|
230
|
+
hubUrl: normalizeHubUrl(hubAddress),
|
|
231
|
+
user: {
|
|
232
|
+
id: String(me.id),
|
|
233
|
+
username: String(me.username ?? ""),
|
|
234
|
+
role: String(me.role ?? "member") as UserRole,
|
|
235
|
+
status: String(me.status ?? "active"),
|
|
236
|
+
groups: Array.isArray(me.groups) ? me.groups : [],
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
} catch { /* fall through to token-only return */ }
|
|
240
|
+
return {
|
|
241
|
+
connected: true,
|
|
242
|
+
hubUrl: normalizeHubUrl(hubAddress),
|
|
243
|
+
user: { id: conn.userId, username: conn.username || "", role: conn.role as UserRole || "member", status: "active" },
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
const realStatus = regResult.status as string;
|
|
247
|
+
store.setClientHubConnection({ ...conn, userToken: "", lastKnownStatus: realStatus });
|
|
248
|
+
return {
|
|
249
|
+
connected: false,
|
|
250
|
+
hubUrl: normalizeHubUrl(hubAddress),
|
|
251
|
+
user: { id: conn.userId, username: conn.username || "", role: "member", status: realStatus },
|
|
252
|
+
};
|
|
253
|
+
} catch { /* registration-status also failed, fall through */ }
|
|
254
|
+
}
|
|
255
|
+
store.setClientHubConnection({ ...conn, userToken: "", lastKnownStatus: "token_expired" });
|
|
256
|
+
return {
|
|
257
|
+
connected: false,
|
|
258
|
+
hubUrl: normalizeHubUrl(hubAddress),
|
|
259
|
+
user: { id: conn.userId, username: conn.username || "", role: "member", status: "token_expired" },
|
|
260
|
+
};
|
|
261
|
+
}
|
|
191
262
|
return { connected: false, user: null };
|
|
192
263
|
}
|
|
193
264
|
}
|
|
@@ -218,12 +289,17 @@ export async function autoJoinHub(
|
|
|
218
289
|
}
|
|
219
290
|
}
|
|
220
291
|
|
|
292
|
+
const persisted = store.getClientHubConnection();
|
|
293
|
+
const existingIdentityKey = persisted?.identityKey || "";
|
|
294
|
+
|
|
221
295
|
log.info(`Joining Hub at ${hubUrl} as "${username}"...`);
|
|
222
296
|
const result = await hubRequestJson(hubUrl, "", "/api/v1/hub/join", {
|
|
223
297
|
method: "POST",
|
|
224
|
-
body: JSON.stringify({ teamToken, username, deviceName: hostname, clientIp }),
|
|
298
|
+
body: JSON.stringify({ teamToken, username, deviceName: hostname, clientIp, identityKey: existingIdentityKey }),
|
|
225
299
|
}) as any;
|
|
226
300
|
|
|
301
|
+
const returnedIdentityKey = String(result.identityKey || existingIdentityKey || "");
|
|
302
|
+
|
|
227
303
|
if (result.status === "pending") {
|
|
228
304
|
log.info(`Join request submitted, awaiting admin approval. userId=${result.userId}`);
|
|
229
305
|
store.setClientHubConnection({
|
|
@@ -233,6 +309,8 @@ export async function autoJoinHub(
|
|
|
233
309
|
userToken: "",
|
|
234
310
|
role: "member",
|
|
235
311
|
connectedAt: Date.now(),
|
|
312
|
+
identityKey: returnedIdentityKey,
|
|
313
|
+
lastKnownStatus: "pending",
|
|
236
314
|
});
|
|
237
315
|
throw new PendingApprovalError(result.userId);
|
|
238
316
|
}
|
|
@@ -241,6 +319,10 @@ export async function autoJoinHub(
|
|
|
241
319
|
throw new Error(`Join request was rejected by the Hub admin.`);
|
|
242
320
|
}
|
|
243
321
|
|
|
322
|
+
if (result.status === "blocked") {
|
|
323
|
+
throw new Error(`Your account has been blocked by the Hub admin.`);
|
|
324
|
+
}
|
|
325
|
+
|
|
244
326
|
if (!result.userToken) {
|
|
245
327
|
throw new Error(`Hub join failed: ${JSON.stringify(result)}`);
|
|
246
328
|
}
|
|
@@ -253,6 +335,8 @@ export async function autoJoinHub(
|
|
|
253
335
|
userToken: result.userToken,
|
|
254
336
|
role: "member",
|
|
255
337
|
connectedAt: Date.now(),
|
|
338
|
+
identityKey: returnedIdentityKey,
|
|
339
|
+
lastKnownStatus: "active",
|
|
256
340
|
});
|
|
257
341
|
return store.getClientHubConnection()!;
|
|
258
342
|
}
|
package/src/config.ts
CHANGED
|
@@ -128,7 +128,8 @@ export function resolveConfig(raw: Partial<MemosLocalConfig> | undefined, stateD
|
|
|
128
128
|
userToken: cfg.sharing?.client?.userToken ?? "",
|
|
129
129
|
teamToken: cfg.sharing?.client?.teamToken ?? "",
|
|
130
130
|
pendingUserId: cfg.sharing?.client?.pendingUserId ?? "",
|
|
131
|
-
|
|
131
|
+
nickname: cfg.sharing?.client?.nickname ?? "",
|
|
132
|
+
} : { hubAddress: "", userToken: "", teamToken: "", pendingUserId: "", nickname: "" };
|
|
132
133
|
return { enabled, role, hub, client, capabilities: sharingCapabilities };
|
|
133
134
|
})(),
|
|
134
135
|
};
|
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
|
}
|