@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/dist/viewer/server.js
CHANGED
|
@@ -72,6 +72,8 @@ class ViewerServer {
|
|
|
72
72
|
authFile;
|
|
73
73
|
auth;
|
|
74
74
|
ctx;
|
|
75
|
+
cookieName;
|
|
76
|
+
defaultHubPort;
|
|
75
77
|
static SESSION_TTL = 24 * 60 * 60 * 1000;
|
|
76
78
|
static PLUGIN_VERSION = (() => {
|
|
77
79
|
try {
|
|
@@ -105,16 +107,31 @@ class ViewerServer {
|
|
|
105
107
|
this.ctx = opts.ctx;
|
|
106
108
|
this.authFile = node_path_1.default.join(opts.dataDir, "viewer-auth.json");
|
|
107
109
|
this.auth = { passwordHash: null, sessions: new Map() };
|
|
110
|
+
this.cookieName = `memos_token_${opts.port}`;
|
|
111
|
+
this.defaultHubPort = opts.defaultHubPort ?? 18800;
|
|
108
112
|
this.resetToken = node_crypto_1.default.randomBytes(16).toString("hex");
|
|
109
113
|
this.loadAuth();
|
|
110
114
|
}
|
|
115
|
+
getHubPort() {
|
|
116
|
+
const configured = this.ctx?.config?.sharing?.hub?.port;
|
|
117
|
+
if (configured && configured !== 18800)
|
|
118
|
+
return configured;
|
|
119
|
+
return this.defaultHubPort;
|
|
120
|
+
}
|
|
111
121
|
start() {
|
|
122
|
+
const MAX_PORT_RETRIES = 5;
|
|
112
123
|
return new Promise((resolve, reject) => {
|
|
124
|
+
let retries = 0;
|
|
113
125
|
this.server = node_http_1.default.createServer((req, res) => this.handleRequest(req, res));
|
|
114
126
|
this.server.on("error", (err) => {
|
|
115
|
-
if (err.code === "EADDRINUSE") {
|
|
116
|
-
|
|
117
|
-
this.
|
|
127
|
+
if (err.code === "EADDRINUSE" && retries < MAX_PORT_RETRIES) {
|
|
128
|
+
retries++;
|
|
129
|
+
const nextPort = this.port + retries;
|
|
130
|
+
this.log.warn(`Viewer port ${this.port + retries - 1} in use, trying ${nextPort}`);
|
|
131
|
+
this.server.listen(nextPort, "0.0.0.0");
|
|
132
|
+
}
|
|
133
|
+
else if (err.code === "EADDRINUSE") {
|
|
134
|
+
reject(new Error(`Viewer failed to find open port after ${MAX_PORT_RETRIES} retries (tried ${this.port}–${this.port + MAX_PORT_RETRIES})`));
|
|
118
135
|
}
|
|
119
136
|
else {
|
|
120
137
|
reject(err);
|
|
@@ -193,7 +210,8 @@ class ViewerServer {
|
|
|
193
210
|
}
|
|
194
211
|
isValidSession(req) {
|
|
195
212
|
const cookie = req.headers.cookie ?? "";
|
|
196
|
-
const
|
|
213
|
+
const re = new RegExp(`${this.cookieName}=([a-f0-9]+)`);
|
|
214
|
+
const match = cookie.match(re);
|
|
197
215
|
if (!match)
|
|
198
216
|
return false;
|
|
199
217
|
const expiry = this.auth.sessions.get(match[1]);
|
|
@@ -313,6 +331,8 @@ class ViewerServer {
|
|
|
313
331
|
this.handleSharingChangeRole(req, res);
|
|
314
332
|
else if (p === "/api/sharing/retry-join" && req.method === "POST")
|
|
315
333
|
this.handleRetryJoin(req, res);
|
|
334
|
+
else if (p === "/api/sharing/leave" && req.method === "POST")
|
|
335
|
+
this.handleLeaveTeam(req, res);
|
|
316
336
|
else if (p === "/api/sharing/search/memories" && req.method === "POST")
|
|
317
337
|
this.handleSharingMemorySearch(req, res);
|
|
318
338
|
else if (p === "/api/sharing/memories/list" && req.method === "GET")
|
|
@@ -353,6 +373,8 @@ class ViewerServer {
|
|
|
353
373
|
this.handleSharingNotificationsRead(req, res);
|
|
354
374
|
else if (p === "/api/sharing/notifications/clear" && req.method === "POST")
|
|
355
375
|
this.handleSharingNotificationsClear(req, res);
|
|
376
|
+
else if (p === "/api/sharing/sync-hub-removal" && req.method === "POST")
|
|
377
|
+
this.handleSyncHubRemoval(req, res);
|
|
356
378
|
else if (p === "/api/notifications/stream" && req.method === "GET")
|
|
357
379
|
this.handleNotifSSE(req, res);
|
|
358
380
|
else if (p === "/api/admin/shared-tasks" && req.method === "GET")
|
|
@@ -440,7 +462,7 @@ class ViewerServer {
|
|
|
440
462
|
const token = this.createSession();
|
|
441
463
|
res.writeHead(200, {
|
|
442
464
|
"Content-Type": "application/json",
|
|
443
|
-
"Set-Cookie":
|
|
465
|
+
"Set-Cookie": `${this.cookieName}=${token}; Path=/; HttpOnly; SameSite=Strict; Max-Age=86400`,
|
|
444
466
|
});
|
|
445
467
|
res.end(JSON.stringify({ ok: true, message: "Password set successfully" }));
|
|
446
468
|
}
|
|
@@ -462,7 +484,7 @@ class ViewerServer {
|
|
|
462
484
|
const token = this.createSession();
|
|
463
485
|
res.writeHead(200, {
|
|
464
486
|
"Content-Type": "application/json",
|
|
465
|
-
"Set-Cookie":
|
|
487
|
+
"Set-Cookie": `${this.cookieName}=${token}; Path=/; HttpOnly; SameSite=Strict; Max-Age=86400`,
|
|
466
488
|
});
|
|
467
489
|
res.end(JSON.stringify({ ok: true }));
|
|
468
490
|
}
|
|
@@ -474,12 +496,13 @@ class ViewerServer {
|
|
|
474
496
|
}
|
|
475
497
|
handleLogout(req, res) {
|
|
476
498
|
const cookie = req.headers.cookie ?? "";
|
|
477
|
-
const
|
|
499
|
+
const re = new RegExp(`${this.cookieName}=([a-f0-9]+)`);
|
|
500
|
+
const match = cookie.match(re);
|
|
478
501
|
if (match)
|
|
479
502
|
this.auth.sessions.delete(match[1]);
|
|
480
503
|
res.writeHead(200, {
|
|
481
504
|
"Content-Type": "application/json",
|
|
482
|
-
"Set-Cookie":
|
|
505
|
+
"Set-Cookie": `${this.cookieName}=; Path=/; HttpOnly; Max-Age=0`,
|
|
483
506
|
});
|
|
484
507
|
res.end(JSON.stringify({ ok: true }));
|
|
485
508
|
}
|
|
@@ -505,7 +528,7 @@ class ViewerServer {
|
|
|
505
528
|
const sessionToken = this.createSession();
|
|
506
529
|
res.writeHead(200, {
|
|
507
530
|
"Content-Type": "application/json",
|
|
508
|
-
"Set-Cookie":
|
|
531
|
+
"Set-Cookie": `${this.cookieName}=${sessionToken}; Path=/; HttpOnly; SameSite=Strict; Max-Age=86400`,
|
|
509
532
|
});
|
|
510
533
|
res.end(JSON.stringify({ ok: true, message: "Password reset successfully" }));
|
|
511
534
|
}
|
|
@@ -543,9 +566,8 @@ class ViewerServer {
|
|
|
543
566
|
params.push(role);
|
|
544
567
|
}
|
|
545
568
|
if (owner && owner.startsWith("agent:")) {
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
params.push(owner, agentPrefix + "%");
|
|
569
|
+
conditions.push("(owner = ? OR owner = 'public')");
|
|
570
|
+
params.push(owner);
|
|
549
571
|
}
|
|
550
572
|
else if (owner) {
|
|
551
573
|
conditions.push("owner = ?");
|
|
@@ -561,7 +583,7 @@ class ViewerServer {
|
|
|
561
583
|
}
|
|
562
584
|
const where = conditions.length > 0 ? " WHERE " + conditions.join(" AND ") : "";
|
|
563
585
|
const totalRow = db.prepare("SELECT COUNT(*) as count FROM chunks" + where).get(...params);
|
|
564
|
-
const rawMemories = db.prepare("SELECT * FROM chunks" + where + ` ORDER BY created_at ${sortBy} LIMIT ? OFFSET ?`).all(...params, limit, offset);
|
|
586
|
+
const rawMemories = db.prepare("SELECT * FROM chunks" + where + ` ORDER BY CASE WHEN dedup_status IN ('duplicate','merged') THEN 1 ELSE 0 END ASC, created_at ${sortBy} LIMIT ? OFFSET ?`).all(...params, limit, offset);
|
|
565
587
|
const findMergeSources = db.prepare("SELECT id, summary, role FROM chunks WHERE dedup_target = ? AND (dedup_status = 'merged' OR dedup_status = 'duplicate')");
|
|
566
588
|
const chunkIds = rawMemories.map((m) => m.id);
|
|
567
589
|
const sharingMap = new Map();
|
|
@@ -572,6 +594,12 @@ class ViewerServer {
|
|
|
572
594
|
const sharedRows = db.prepare(`SELECT source_chunk_id, visibility, group_id FROM hub_memories WHERE source_chunk_id IN (${placeholders})`).all(...chunkIds);
|
|
573
595
|
for (const r of sharedRows)
|
|
574
596
|
sharingMap.set(r.source_chunk_id, r);
|
|
597
|
+
const teamMetaRows = db.prepare(`SELECT chunk_id, visibility, group_id FROM team_shared_chunks WHERE chunk_id IN (${placeholders})`).all(...chunkIds);
|
|
598
|
+
for (const r of teamMetaRows) {
|
|
599
|
+
if (!sharingMap.has(r.chunk_id)) {
|
|
600
|
+
sharingMap.set(r.chunk_id, { visibility: r.visibility, group_id: r.group_id });
|
|
601
|
+
}
|
|
602
|
+
}
|
|
575
603
|
const localRows = db.prepare(`SELECT chunk_id, original_owner, shared_at FROM local_shared_memories WHERE chunk_id IN (${placeholders})`).all(...chunkIds);
|
|
576
604
|
for (const r of localRows)
|
|
577
605
|
localShareMap.set(r.chunk_id, r);
|
|
@@ -728,9 +756,8 @@ class ViewerServer {
|
|
|
728
756
|
let sessionQuery;
|
|
729
757
|
let sessionParams;
|
|
730
758
|
if (ownerFilter && ownerFilter.startsWith("agent:")) {
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
sessionParams = [ownerFilter, agentPrefix + "%"];
|
|
759
|
+
sessionQuery = "SELECT session_key, COUNT(*) as count, MIN(created_at) as earliest, MAX(created_at) as latest FROM chunks WHERE (owner = ? OR owner = 'public') GROUP BY session_key ORDER BY latest DESC";
|
|
760
|
+
sessionParams = [ownerFilter];
|
|
734
761
|
}
|
|
735
762
|
else if (ownerFilter) {
|
|
736
763
|
sessionQuery = "SELECT session_key, COUNT(*) as count, MIN(created_at) as earliest, MAX(created_at) as latest FROM chunks WHERE owner = ? GROUP BY session_key ORDER BY latest DESC";
|
|
@@ -1162,7 +1189,7 @@ class ViewerServer {
|
|
|
1162
1189
|
}
|
|
1163
1190
|
});
|
|
1164
1191
|
}
|
|
1165
|
-
handleSkillDelete(res, urlPath) {
|
|
1192
|
+
async handleSkillDelete(res, urlPath) {
|
|
1166
1193
|
const skillId = urlPath.replace("/api/skill/", "");
|
|
1167
1194
|
const skill = this.store.getSkill(skillId);
|
|
1168
1195
|
if (!skill) {
|
|
@@ -1170,7 +1197,18 @@ class ViewerServer {
|
|
|
1170
1197
|
res.end(JSON.stringify({ error: "Skill not found" }));
|
|
1171
1198
|
return;
|
|
1172
1199
|
}
|
|
1173
|
-
|
|
1200
|
+
try {
|
|
1201
|
+
const hub = this.resolveHubConnection();
|
|
1202
|
+
if (hub) {
|
|
1203
|
+
await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/skills/unpublish", {
|
|
1204
|
+
method: "POST",
|
|
1205
|
+
body: JSON.stringify({ sourceSkillId: skillId }),
|
|
1206
|
+
}).catch(() => { });
|
|
1207
|
+
}
|
|
1208
|
+
const db = this.store.db;
|
|
1209
|
+
db.prepare("DELETE FROM hub_skills WHERE source_skill_id = ?").run(skillId);
|
|
1210
|
+
}
|
|
1211
|
+
catch (_) { }
|
|
1174
1212
|
try {
|
|
1175
1213
|
if (skill.dirPath && node_fs_1.default.existsSync(skill.dirPath)) {
|
|
1176
1214
|
node_fs_1.default.rmSync(skill.dirPath, { recursive: true, force: true });
|
|
@@ -1339,8 +1377,6 @@ class ViewerServer {
|
|
|
1339
1377
|
}
|
|
1340
1378
|
let hubSynced = false;
|
|
1341
1379
|
if (scope === "team") {
|
|
1342
|
-
if (!isLocalShared)
|
|
1343
|
-
this.store.markMemorySharedLocally(chunkId);
|
|
1344
1380
|
if (!isTeamShared) {
|
|
1345
1381
|
const hubClient = await this.resolveHubClientAware();
|
|
1346
1382
|
const refreshedChunk = db.prepare("SELECT * FROM chunks WHERE id = ?").get(chunkId);
|
|
@@ -1348,18 +1384,29 @@ class ViewerServer {
|
|
|
1348
1384
|
method: "POST",
|
|
1349
1385
|
body: JSON.stringify({ memory: { sourceChunkId: refreshedChunk.id, role: refreshedChunk.role, content: refreshedChunk.content, summary: refreshedChunk.summary, kind: refreshedChunk.kind, groupId: null, visibility: "public" } }),
|
|
1350
1386
|
});
|
|
1351
|
-
if (
|
|
1387
|
+
if (!isLocalShared)
|
|
1388
|
+
this.store.markMemorySharedLocally(chunkId);
|
|
1389
|
+
const memoryId = String(response?.memoryId ?? "");
|
|
1390
|
+
const isHubRole = this.ctx?.config?.sharing?.role === "hub";
|
|
1391
|
+
if (hubClient.userId && isHubRole) {
|
|
1352
1392
|
const existing = this.store.getHubMemoryBySource(hubClient.userId, chunkId);
|
|
1353
1393
|
this.store.upsertHubMemory({
|
|
1354
|
-
id:
|
|
1394
|
+
id: memoryId || existing?.id || node_crypto_1.default.randomUUID(),
|
|
1355
1395
|
sourceChunkId: chunkId, sourceUserId: hubClient.userId,
|
|
1356
1396
|
role: refreshedChunk.role, content: refreshedChunk.content, summary: refreshedChunk.summary ?? "",
|
|
1357
1397
|
kind: refreshedChunk.kind, groupId: null, visibility: "public",
|
|
1358
1398
|
createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(),
|
|
1359
1399
|
});
|
|
1360
1400
|
}
|
|
1401
|
+
else if (hubClient.userId) {
|
|
1402
|
+
this.store.upsertTeamSharedChunk(chunkId, { hubMemoryId: memoryId, visibility: "public", groupId: null });
|
|
1403
|
+
}
|
|
1361
1404
|
hubSynced = true;
|
|
1362
1405
|
}
|
|
1406
|
+
else {
|
|
1407
|
+
if (!isLocalShared)
|
|
1408
|
+
this.store.markMemorySharedLocally(chunkId);
|
|
1409
|
+
}
|
|
1363
1410
|
}
|
|
1364
1411
|
else if (scope === "local") {
|
|
1365
1412
|
if (isTeamShared) {
|
|
@@ -1370,6 +1417,7 @@ class ViewerServer {
|
|
|
1370
1417
|
});
|
|
1371
1418
|
if (hubClient.userId)
|
|
1372
1419
|
this.store.deleteHubMemoryBySource(hubClient.userId, chunkId);
|
|
1420
|
+
this.store.deleteTeamSharedChunk(chunkId);
|
|
1373
1421
|
hubSynced = true;
|
|
1374
1422
|
}
|
|
1375
1423
|
catch (err) {
|
|
@@ -1388,6 +1436,7 @@ class ViewerServer {
|
|
|
1388
1436
|
});
|
|
1389
1437
|
if (hubClient.userId)
|
|
1390
1438
|
this.store.deleteHubMemoryBySource(hubClient.userId, chunkId);
|
|
1439
|
+
this.store.deleteTeamSharedChunk(chunkId);
|
|
1391
1440
|
hubSynced = true;
|
|
1392
1441
|
}
|
|
1393
1442
|
catch (err) {
|
|
@@ -1427,14 +1476,6 @@ class ViewerServer {
|
|
|
1427
1476
|
return this.jsonResponse(res, { ok: true, scope, changed: false });
|
|
1428
1477
|
}
|
|
1429
1478
|
let hubSynced = false;
|
|
1430
|
-
if (scope === "local" || scope === "team") {
|
|
1431
|
-
if (!isLocalShared) {
|
|
1432
|
-
const originalOwner = task.owner;
|
|
1433
|
-
const db = this.store.db;
|
|
1434
|
-
db.prepare("INSERT INTO local_shared_tasks (task_id, hub_task_id, original_owner, shared_at) VALUES (?, ?, ?, ?) ON CONFLICT(task_id) DO UPDATE SET original_owner = excluded.original_owner, shared_at = excluded.shared_at").run(taskId, "", originalOwner, Date.now());
|
|
1435
|
-
db.prepare("UPDATE tasks SET owner = 'public' WHERE id = ?").run(taskId);
|
|
1436
|
-
}
|
|
1437
|
-
}
|
|
1438
1479
|
if (scope === "team") {
|
|
1439
1480
|
if (!isTeamShared) {
|
|
1440
1481
|
const chunks = this.store.getChunksByTask(taskId);
|
|
@@ -1458,6 +1499,20 @@ class ViewerServer {
|
|
|
1458
1499
|
}
|
|
1459
1500
|
hubSynced = true;
|
|
1460
1501
|
}
|
|
1502
|
+
if (!isLocalShared) {
|
|
1503
|
+
const originalOwner = task.owner;
|
|
1504
|
+
const db = this.store.db;
|
|
1505
|
+
db.prepare("INSERT INTO local_shared_tasks (task_id, hub_task_id, original_owner, shared_at) VALUES (?, ?, ?, ?) ON CONFLICT(task_id) DO UPDATE SET original_owner = excluded.original_owner, shared_at = excluded.shared_at").run(taskId, "", originalOwner, Date.now());
|
|
1506
|
+
db.prepare("UPDATE tasks SET owner = 'public' WHERE id = ?").run(taskId);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
if (scope === "local") {
|
|
1510
|
+
if (!isLocalShared) {
|
|
1511
|
+
const originalOwner = task.owner;
|
|
1512
|
+
const db = this.store.db;
|
|
1513
|
+
db.prepare("INSERT INTO local_shared_tasks (task_id, hub_task_id, original_owner, shared_at) VALUES (?, ?, ?, ?) ON CONFLICT(task_id) DO UPDATE SET original_owner = excluded.original_owner, shared_at = excluded.shared_at").run(taskId, "", originalOwner, Date.now());
|
|
1514
|
+
db.prepare("UPDATE tasks SET owner = 'public' WHERE id = ?").run(taskId);
|
|
1515
|
+
}
|
|
1461
1516
|
}
|
|
1462
1517
|
if (scope === "local" && isTeamShared) {
|
|
1463
1518
|
try {
|
|
@@ -1528,10 +1583,6 @@ class ViewerServer {
|
|
|
1528
1583
|
return this.jsonResponse(res, { ok: true, scope, changed: false });
|
|
1529
1584
|
}
|
|
1530
1585
|
let hubSynced = false;
|
|
1531
|
-
if (scope === "local" || scope === "team") {
|
|
1532
|
-
if (!isLocalShared)
|
|
1533
|
-
this.store.setSkillVisibility(skillId, "public");
|
|
1534
|
-
}
|
|
1535
1586
|
if (scope === "team") {
|
|
1536
1587
|
if (!isTeamShared) {
|
|
1537
1588
|
const bundle = (0, skill_sync_1.buildSkillBundleForHub)(this.store, skillId);
|
|
@@ -1553,6 +1604,12 @@ class ViewerServer {
|
|
|
1553
1604
|
}
|
|
1554
1605
|
hubSynced = true;
|
|
1555
1606
|
}
|
|
1607
|
+
if (!isLocalShared)
|
|
1608
|
+
this.store.setSkillVisibility(skillId, "public");
|
|
1609
|
+
}
|
|
1610
|
+
if (scope === "local") {
|
|
1611
|
+
if (!isLocalShared)
|
|
1612
|
+
this.store.setSkillVisibility(skillId, "public");
|
|
1556
1613
|
}
|
|
1557
1614
|
if (scope === "local" && isTeamShared) {
|
|
1558
1615
|
try {
|
|
@@ -1595,7 +1652,18 @@ class ViewerServer {
|
|
|
1595
1652
|
}
|
|
1596
1653
|
getHubMemoryForChunk(chunkId) {
|
|
1597
1654
|
const db = this.store.db;
|
|
1598
|
-
|
|
1655
|
+
const hub = db.prepare("SELECT * FROM hub_memories WHERE source_chunk_id = ? LIMIT 1").get(chunkId);
|
|
1656
|
+
if (hub)
|
|
1657
|
+
return hub;
|
|
1658
|
+
const ts = this.store.getTeamSharedChunk(chunkId);
|
|
1659
|
+
if (ts) {
|
|
1660
|
+
return {
|
|
1661
|
+
source_chunk_id: chunkId,
|
|
1662
|
+
visibility: ts.visibility,
|
|
1663
|
+
group_id: ts.groupId,
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
return undefined;
|
|
1599
1667
|
}
|
|
1600
1668
|
getHubTaskForLocal(taskId) {
|
|
1601
1669
|
const db = this.store.db;
|
|
@@ -1641,6 +1709,8 @@ class ViewerServer {
|
|
|
1641
1709
|
// ─── Helpers ───
|
|
1642
1710
|
// ─── Config API ───
|
|
1643
1711
|
getOpenClawConfigPath() {
|
|
1712
|
+
if (process.env.OPENCLAW_CONFIG_PATH)
|
|
1713
|
+
return process.env.OPENCLAW_CONFIG_PATH;
|
|
1644
1714
|
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
1645
1715
|
const ocHome = process.env.OPENCLAW_STATE_DIR || node_path_1.default.join(home, ".openclaw");
|
|
1646
1716
|
return node_path_1.default.join(ocHome, "openclaw.json");
|
|
@@ -1723,7 +1793,18 @@ class ViewerServer {
|
|
|
1723
1793
|
base.connection.apiVersion = info?.apiVersion ?? null;
|
|
1724
1794
|
}
|
|
1725
1795
|
catch { /* ignore */ }
|
|
1726
|
-
|
|
1796
|
+
const hubStats = { totalMembers: 0, onlineMembers: 0, pendingMembers: 0 };
|
|
1797
|
+
try {
|
|
1798
|
+
const activeUsers = this.store.listHubUsers("active");
|
|
1799
|
+
const pendingUsers = this.store.listHubUsers("pending");
|
|
1800
|
+
const now = Date.now();
|
|
1801
|
+
const OFFLINE_THRESHOLD = 120_000;
|
|
1802
|
+
hubStats.totalMembers = activeUsers.length;
|
|
1803
|
+
hubStats.onlineMembers = activeUsers.filter(u => u.lastActiveAt && (now - u.lastActiveAt < OFFLINE_THRESHOLD)).length;
|
|
1804
|
+
hubStats.pendingMembers = pendingUsers.length;
|
|
1805
|
+
}
|
|
1806
|
+
catch { /* best-effort */ }
|
|
1807
|
+
this.jsonResponse(res, { ...base, hubStats });
|
|
1727
1808
|
return;
|
|
1728
1809
|
}
|
|
1729
1810
|
const hasPendingConnection = Boolean(persisted?.hubUrl && persisted?.userId && !persisted?.userToken);
|
|
@@ -1740,6 +1821,9 @@ class ViewerServer {
|
|
|
1740
1821
|
if (status.user?.status === "rejected") {
|
|
1741
1822
|
output.connection.rejected = true;
|
|
1742
1823
|
}
|
|
1824
|
+
if (status.user?.status === "removed") {
|
|
1825
|
+
output.connection.removed = true;
|
|
1826
|
+
}
|
|
1743
1827
|
if (status.connected && status.hubUrl) {
|
|
1744
1828
|
try {
|
|
1745
1829
|
const info = await fetch(`${status.hubUrl}/api/v1/hub/info`).then((r) => (r.ok ? r.json() : null)).catch(() => null);
|
|
@@ -1873,7 +1957,16 @@ class ViewerServer {
|
|
|
1873
1957
|
this.jsonResponse(res, { ok: true, result });
|
|
1874
1958
|
}
|
|
1875
1959
|
catch (err) {
|
|
1876
|
-
|
|
1960
|
+
const errStr = String(err);
|
|
1961
|
+
if (errStr.includes("username_taken")) {
|
|
1962
|
+
this.jsonResponse(res, { ok: false, error: "username_taken" });
|
|
1963
|
+
}
|
|
1964
|
+
else if (errStr.includes("invalid_params")) {
|
|
1965
|
+
this.jsonResponse(res, { ok: false, error: "invalid_params" });
|
|
1966
|
+
}
|
|
1967
|
+
else {
|
|
1968
|
+
this.jsonResponse(res, { ok: false, error: errStr });
|
|
1969
|
+
}
|
|
1877
1970
|
}
|
|
1878
1971
|
});
|
|
1879
1972
|
}
|
|
@@ -1892,23 +1985,17 @@ class ViewerServer {
|
|
|
1892
1985
|
}
|
|
1893
1986
|
try {
|
|
1894
1987
|
const hubUrl = (0, hub_1.normalizeHubUrl)(hubAddress);
|
|
1895
|
-
const localIPs = this.getLocalIPs();
|
|
1896
|
-
localIPs.push("127.0.0.1", "localhost", "0.0.0.0");
|
|
1897
|
-
try {
|
|
1898
|
-
const u = new URL(hubUrl);
|
|
1899
|
-
if (localIPs.includes(u.hostname)) {
|
|
1900
|
-
return this.jsonResponse(res, { ok: false, error: "cannot_join_self" });
|
|
1901
|
-
}
|
|
1902
|
-
}
|
|
1903
|
-
catch { }
|
|
1904
1988
|
const os = await Promise.resolve().then(() => __importStar(require("os")));
|
|
1905
1989
|
const nickname = sharing.client?.nickname;
|
|
1906
1990
|
const username = nickname || os.userInfo().username || "user";
|
|
1907
1991
|
const hostname = os.hostname() || "unknown";
|
|
1992
|
+
const persisted = this.store.getClientHubConnection();
|
|
1993
|
+
const existingIdentityKey = persisted?.identityKey || "";
|
|
1908
1994
|
const result = await (0, hub_1.hubRequestJson)(hubUrl, "", "/api/v1/hub/join", {
|
|
1909
1995
|
method: "POST",
|
|
1910
|
-
body: JSON.stringify({ teamToken, username, deviceName: hostname, reapply: true }),
|
|
1996
|
+
body: JSON.stringify({ teamToken, username, deviceName: hostname, reapply: true, identityKey: existingIdentityKey }),
|
|
1911
1997
|
});
|
|
1998
|
+
const returnedIdentityKey = String(result.identityKey || existingIdentityKey || "");
|
|
1912
1999
|
this.store.setClientHubConnection({
|
|
1913
2000
|
hubUrl,
|
|
1914
2001
|
userId: String(result.userId || ""),
|
|
@@ -1916,6 +2003,8 @@ class ViewerServer {
|
|
|
1916
2003
|
userToken: result.userToken || "",
|
|
1917
2004
|
role: "member",
|
|
1918
2005
|
connectedAt: Date.now(),
|
|
2006
|
+
identityKey: returnedIdentityKey,
|
|
2007
|
+
lastKnownStatus: result.status || "",
|
|
1919
2008
|
});
|
|
1920
2009
|
this.jsonResponse(res, { ok: true, status: result.status || "pending" });
|
|
1921
2010
|
}
|
|
@@ -2211,14 +2300,14 @@ class ViewerServer {
|
|
|
2211
2300
|
},
|
|
2212
2301
|
}),
|
|
2213
2302
|
});
|
|
2214
|
-
const
|
|
2215
|
-
if (
|
|
2303
|
+
const mid = String(response?.memoryId ?? "");
|
|
2304
|
+
if (hubClient.userId && this.ctx?.config?.sharing?.role === "hub") {
|
|
2216
2305
|
const now = Date.now();
|
|
2217
|
-
const existing = this.store.getHubMemoryBySource(
|
|
2306
|
+
const existing = this.store.getHubMemoryBySource(hubClient.userId, chunk.id);
|
|
2218
2307
|
this.store.upsertHubMemory({
|
|
2219
|
-
id:
|
|
2308
|
+
id: mid || existing?.id || node_crypto_1.default.randomUUID(),
|
|
2220
2309
|
sourceChunkId: chunk.id,
|
|
2221
|
-
sourceUserId:
|
|
2310
|
+
sourceUserId: hubClient.userId,
|
|
2222
2311
|
role: chunk.role,
|
|
2223
2312
|
content: chunk.content,
|
|
2224
2313
|
summary: chunk.summary ?? "",
|
|
@@ -2229,6 +2318,9 @@ class ViewerServer {
|
|
|
2229
2318
|
updatedAt: now,
|
|
2230
2319
|
});
|
|
2231
2320
|
}
|
|
2321
|
+
else if (hubClient.userId) {
|
|
2322
|
+
this.store.upsertTeamSharedChunk(chunk.id, { hubMemoryId: mid, visibility, groupId });
|
|
2323
|
+
}
|
|
2232
2324
|
this.jsonResponse(res, { ok: true, chunkId, visibility, response });
|
|
2233
2325
|
}
|
|
2234
2326
|
catch (err) {
|
|
@@ -2251,6 +2343,7 @@ class ViewerServer {
|
|
|
2251
2343
|
const hubUserId = hubClient.userId;
|
|
2252
2344
|
if (hubUserId)
|
|
2253
2345
|
this.store.deleteHubMemoryBySource(hubUserId, chunkId);
|
|
2346
|
+
this.store.deleteTeamSharedChunk(chunkId);
|
|
2254
2347
|
this.jsonResponse(res, { ok: true, chunkId });
|
|
2255
2348
|
}
|
|
2256
2349
|
catch (err) {
|
|
@@ -2353,7 +2446,7 @@ class ViewerServer {
|
|
|
2353
2446
|
// Hub 模式:连接自己,用 bootstrap admin token
|
|
2354
2447
|
const sharing = this.ctx.config.sharing;
|
|
2355
2448
|
if (sharing?.role === "hub") {
|
|
2356
|
-
const hubPort =
|
|
2449
|
+
const hubPort = this.getHubPort();
|
|
2357
2450
|
const hubUrl = `http://127.0.0.1:${hubPort}`;
|
|
2358
2451
|
try {
|
|
2359
2452
|
const authPath = node_path_1.default.join(this.dataDir, "hub-auth.json");
|
|
@@ -2595,6 +2688,32 @@ class ViewerServer {
|
|
|
2595
2688
|
}
|
|
2596
2689
|
});
|
|
2597
2690
|
}
|
|
2691
|
+
/** Badge-only: clear Client team-share UI metadata when Hub admin removes that memory. Does NOT touch chunks, embeddings, or hub_memories (recall paths). */
|
|
2692
|
+
handleSyncHubRemoval(req, res) {
|
|
2693
|
+
this.readBody(req, (body) => {
|
|
2694
|
+
try {
|
|
2695
|
+
const parsed = JSON.parse(body || "{}");
|
|
2696
|
+
const sourceChunkId = String(parsed.sourceChunkId || "");
|
|
2697
|
+
const memoryIdFromNotif = parsed.memoryId != null && parsed.memoryId !== "" ? String(parsed.memoryId) : "";
|
|
2698
|
+
if (!sourceChunkId)
|
|
2699
|
+
return this.jsonResponse(res, { ok: false, error: "missing_source_chunk_id" }, 400);
|
|
2700
|
+
// Admin removal notifications stay in the feed; if the user re-shared, team_shared_chunks has a new hub_memory_id.
|
|
2701
|
+
// Only clear the badge when this notification refers to the same Hub row we still track (or no id — legacy).
|
|
2702
|
+
if (memoryIdFromNotif) {
|
|
2703
|
+
const current = this.store.getTeamSharedChunk(sourceChunkId);
|
|
2704
|
+
const curId = current?.hubMemoryId ? String(current.hubMemoryId) : "";
|
|
2705
|
+
if (curId && curId !== memoryIdFromNotif) {
|
|
2706
|
+
return this.jsonResponse(res, { ok: true, sourceChunkId, skipped: true, reason: "stale_notification_re_shared" });
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
this.store.deleteTeamSharedChunk(sourceChunkId);
|
|
2710
|
+
this.jsonResponse(res, { ok: true, sourceChunkId });
|
|
2711
|
+
}
|
|
2712
|
+
catch (e) {
|
|
2713
|
+
this.jsonResponse(res, { ok: false, error: String(e) }, 500);
|
|
2714
|
+
}
|
|
2715
|
+
});
|
|
2716
|
+
}
|
|
2598
2717
|
handleNotifSSE(req, res) {
|
|
2599
2718
|
res.writeHead(200, {
|
|
2600
2719
|
"Content-Type": "text/event-stream",
|
|
@@ -2606,6 +2725,8 @@ class ViewerServer {
|
|
|
2606
2725
|
this.notifSSEClients.push(res);
|
|
2607
2726
|
if (!this.notifPollTimer)
|
|
2608
2727
|
this.startNotifPoll();
|
|
2728
|
+
else
|
|
2729
|
+
this.notifPollImmediate();
|
|
2609
2730
|
req.on("close", () => {
|
|
2610
2731
|
this.notifSSEClients = this.notifSSEClients.filter((c) => c !== res);
|
|
2611
2732
|
if (this.notifSSEClients.length === 0)
|
|
@@ -2649,6 +2770,20 @@ class ViewerServer {
|
|
|
2649
2770
|
this.notifPollTimer = undefined;
|
|
2650
2771
|
}
|
|
2651
2772
|
}
|
|
2773
|
+
notifPollImmediate() {
|
|
2774
|
+
const hub = this.resolveHubConnection();
|
|
2775
|
+
if (!hub)
|
|
2776
|
+
return;
|
|
2777
|
+
(0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications?unread=1")
|
|
2778
|
+
.then((data) => {
|
|
2779
|
+
const count = data?.unreadCount ?? 0;
|
|
2780
|
+
if (count !== this.lastKnownNotifCount) {
|
|
2781
|
+
this.lastKnownNotifCount = count;
|
|
2782
|
+
this.broadcastNotifSSE({ type: "update", unreadCount: count });
|
|
2783
|
+
}
|
|
2784
|
+
})
|
|
2785
|
+
.catch(() => { });
|
|
2786
|
+
}
|
|
2652
2787
|
startHubHeartbeat() {
|
|
2653
2788
|
this.stopHubHeartbeat();
|
|
2654
2789
|
const sendHeartbeat = async () => {
|
|
@@ -2773,12 +2908,14 @@ class ViewerServer {
|
|
|
2773
2908
|
if (merged.role === "client" && merged.client) {
|
|
2774
2909
|
const clientCfg = merged.client;
|
|
2775
2910
|
const addr = String(clientCfg.hubAddress || "");
|
|
2776
|
-
if (addr) {
|
|
2911
|
+
if (addr && oldSharingRole === "hub" && oldSharingEnabled) {
|
|
2912
|
+
const selfHubPort = oldSharing?.hub?.port ?? 18800;
|
|
2777
2913
|
const localIPs = this.getLocalIPs();
|
|
2778
2914
|
localIPs.push("127.0.0.1", "localhost", "0.0.0.0");
|
|
2779
2915
|
try {
|
|
2780
2916
|
const u = new URL(addr.startsWith("http") ? addr : `http://${addr}`);
|
|
2781
|
-
|
|
2917
|
+
const targetPort = u.port || (u.protocol === "https:" ? "443" : "80");
|
|
2918
|
+
if (localIPs.includes(u.hostname) && targetPort === String(selfHubPort)) {
|
|
2782
2919
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2783
2920
|
res.end(JSON.stringify({ error: "cannot_join_self" }));
|
|
2784
2921
|
return;
|
|
@@ -2801,24 +2938,26 @@ class ViewerServer {
|
|
|
2801
2938
|
const wasClient = oldSharingEnabled && oldSharingRole === "client";
|
|
2802
2939
|
const isClient = newEnabled && newRole === "client";
|
|
2803
2940
|
if (wasClient && !isClient) {
|
|
2804
|
-
this.
|
|
2941
|
+
await this.withdrawOrLeaveHub();
|
|
2805
2942
|
this.store.clearClientHubConnection();
|
|
2806
|
-
this.log.info("
|
|
2943
|
+
this.log.info("Client hub connection cleared (sharing disabled or role changed)");
|
|
2807
2944
|
}
|
|
2808
|
-
// Detect switching to a different Hub while still in client mode
|
|
2809
2945
|
if (wasClient && isClient) {
|
|
2810
2946
|
const newClientAddr = String(merged.client?.hubAddress || "");
|
|
2811
2947
|
if (newClientAddr && oldClientHubAddress && (0, hub_1.normalizeHubUrl)(newClientAddr) !== (0, hub_1.normalizeHubUrl)(oldClientHubAddress)) {
|
|
2812
2948
|
this.notifyHubLeave();
|
|
2813
|
-
this.store.
|
|
2814
|
-
|
|
2949
|
+
const oldConn = this.store.getClientHubConnection();
|
|
2950
|
+
if (oldConn) {
|
|
2951
|
+
this.store.setClientHubConnection({ ...oldConn, hubUrl: (0, hub_1.normalizeHubUrl)(newClientAddr), userToken: "", lastKnownStatus: "hub_changed" });
|
|
2952
|
+
}
|
|
2953
|
+
this.log.info("Client hub connection token cleared (switched to different Hub), identity preserved");
|
|
2815
2954
|
}
|
|
2816
2955
|
}
|
|
2817
2956
|
if (merged.role === "hub") {
|
|
2818
2957
|
merged.client = { hubAddress: "", userToken: "", teamToken: "" };
|
|
2819
2958
|
}
|
|
2820
2959
|
else if (merged.role === "client") {
|
|
2821
|
-
merged.hub = {
|
|
2960
|
+
merged.hub = { teamName: "", teamToken: "" };
|
|
2822
2961
|
}
|
|
2823
2962
|
config.sharing = merged;
|
|
2824
2963
|
}
|
|
@@ -2826,12 +2965,29 @@ class ViewerServer {
|
|
|
2826
2965
|
node_fs_1.default.writeFileSync(cfgPath, JSON.stringify(raw, null, 2), "utf-8");
|
|
2827
2966
|
this.log.info("Plugin config updated via Viewer");
|
|
2828
2967
|
this.stopHubHeartbeat();
|
|
2829
|
-
// When switching to client mode,
|
|
2968
|
+
// When switching to client mode or re-enabling sharing as client, send join request
|
|
2830
2969
|
const finalSharing = config.sharing;
|
|
2831
|
-
|
|
2832
|
-
|
|
2970
|
+
const nowClient = Boolean(finalSharing?.enabled) && finalSharing?.role === "client";
|
|
2971
|
+
const previouslyClient = oldSharingEnabled && oldSharingRole === "client";
|
|
2972
|
+
let joinStatus;
|
|
2973
|
+
if (nowClient && !previouslyClient) {
|
|
2974
|
+
try {
|
|
2975
|
+
joinStatus = await this.autoJoinOnSave(finalSharing);
|
|
2976
|
+
}
|
|
2977
|
+
catch (e) {
|
|
2978
|
+
this.log.warn(`Auto-join on save failed: ${e}`);
|
|
2979
|
+
}
|
|
2833
2980
|
}
|
|
2834
|
-
this.jsonResponse(res, { ok: true });
|
|
2981
|
+
this.jsonResponse(res, { ok: true, joinStatus, restart: true });
|
|
2982
|
+
setTimeout(() => {
|
|
2983
|
+
this.log.info("config-save: triggering gateway restart via SIGUSR1...");
|
|
2984
|
+
try {
|
|
2985
|
+
process.kill(process.pid, "SIGUSR1");
|
|
2986
|
+
}
|
|
2987
|
+
catch (sig) {
|
|
2988
|
+
this.log.warn(`SIGUSR1 failed: ${sig}`);
|
|
2989
|
+
}
|
|
2990
|
+
}, 500);
|
|
2835
2991
|
}
|
|
2836
2992
|
catch (e) {
|
|
2837
2993
|
this.log.warn(`handleSaveConfig error: ${e}`);
|
|
@@ -2845,16 +3001,19 @@ class ViewerServer {
|
|
|
2845
3001
|
const hubAddress = String(clientCfg?.hubAddress || "");
|
|
2846
3002
|
const teamToken = String(clientCfg?.teamToken || "");
|
|
2847
3003
|
if (!hubAddress || !teamToken)
|
|
2848
|
-
return;
|
|
3004
|
+
return undefined;
|
|
2849
3005
|
const hubUrl = (0, hub_1.normalizeHubUrl)(hubAddress);
|
|
2850
3006
|
const os = await Promise.resolve().then(() => __importStar(require("os")));
|
|
2851
3007
|
const nickname = String(clientCfg?.nickname || "");
|
|
2852
3008
|
const username = nickname || os.userInfo().username || "user";
|
|
2853
3009
|
const hostname = os.hostname() || "unknown";
|
|
3010
|
+
const persisted = this.store.getClientHubConnection();
|
|
3011
|
+
const existingIdentityKey = persisted?.identityKey || "";
|
|
2854
3012
|
const result = await (0, hub_1.hubRequestJson)(hubUrl, "", "/api/v1/hub/join", {
|
|
2855
3013
|
method: "POST",
|
|
2856
|
-
body: JSON.stringify({ teamToken, username, deviceName: hostname }),
|
|
3014
|
+
body: JSON.stringify({ teamToken, username, deviceName: hostname, identityKey: existingIdentityKey }),
|
|
2857
3015
|
});
|
|
3016
|
+
const returnedIdentityKey = String(result.identityKey || existingIdentityKey || "");
|
|
2858
3017
|
this.store.setClientHubConnection({
|
|
2859
3018
|
hubUrl,
|
|
2860
3019
|
userId: String(result.userId || ""),
|
|
@@ -2862,11 +3021,51 @@ class ViewerServer {
|
|
|
2862
3021
|
userToken: result.userToken || "",
|
|
2863
3022
|
role: "member",
|
|
2864
3023
|
connectedAt: Date.now(),
|
|
3024
|
+
identityKey: returnedIdentityKey,
|
|
3025
|
+
lastKnownStatus: result.status || "",
|
|
2865
3026
|
});
|
|
2866
3027
|
this.log.info(`Auto-join on save: status=${result.status}, userId=${result.userId}`);
|
|
2867
3028
|
if (result.userToken) {
|
|
2868
3029
|
this.startHubHeartbeat();
|
|
2869
3030
|
}
|
|
3031
|
+
return result.status;
|
|
3032
|
+
}
|
|
3033
|
+
handleLeaveTeam(_req, res) {
|
|
3034
|
+
this.readBody(_req, async () => {
|
|
3035
|
+
try {
|
|
3036
|
+
await this.withdrawOrLeaveHub();
|
|
3037
|
+
this.store.clearClientHubConnection();
|
|
3038
|
+
const configPath = this.getOpenClawConfigPath();
|
|
3039
|
+
if (configPath && node_fs_1.default.existsSync(configPath)) {
|
|
3040
|
+
const raw = JSON.parse(node_fs_1.default.readFileSync(configPath, "utf8"));
|
|
3041
|
+
const pluginKey = Object.keys(raw.plugins?.entries ?? {}).find(k => k.includes("memos-local"));
|
|
3042
|
+
if (pluginKey) {
|
|
3043
|
+
const cfg = raw.plugins.entries[pluginKey].config ?? {};
|
|
3044
|
+
if (cfg.sharing) {
|
|
3045
|
+
cfg.sharing.enabled = false;
|
|
3046
|
+
cfg.sharing.client = { hubAddress: "", userToken: "", teamToken: "" };
|
|
3047
|
+
}
|
|
3048
|
+
raw.plugins.entries[pluginKey].config = cfg;
|
|
3049
|
+
node_fs_1.default.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
|
|
3050
|
+
this.log.info("handleLeaveTeam: config updated, sharing disabled");
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
this.jsonResponse(res, { ok: true, restart: true });
|
|
3054
|
+
setTimeout(() => {
|
|
3055
|
+
this.log.info("handleLeaveTeam: triggering gateway restart via SIGUSR1...");
|
|
3056
|
+
try {
|
|
3057
|
+
process.kill(process.pid, "SIGUSR1");
|
|
3058
|
+
}
|
|
3059
|
+
catch (sig) {
|
|
3060
|
+
this.log.warn(`SIGUSR1 failed: ${sig}`);
|
|
3061
|
+
}
|
|
3062
|
+
}, 500);
|
|
3063
|
+
}
|
|
3064
|
+
catch (e) {
|
|
3065
|
+
this.log.warn(`handleLeaveTeam error: ${e}`);
|
|
3066
|
+
this.jsonResponse(res, { ok: false, error: String(e) });
|
|
3067
|
+
}
|
|
3068
|
+
});
|
|
2870
3069
|
}
|
|
2871
3070
|
async notifyHubLeave() {
|
|
2872
3071
|
try {
|
|
@@ -2886,12 +3085,46 @@ class ViewerServer {
|
|
|
2886
3085
|
this.log.warn(`Failed to notify Hub of leave: ${e}`);
|
|
2887
3086
|
}
|
|
2888
3087
|
}
|
|
3088
|
+
async withdrawOrLeaveHub() {
|
|
3089
|
+
try {
|
|
3090
|
+
const persisted = this.store.getClientHubConnection();
|
|
3091
|
+
const sharing = this.ctx?.config?.sharing;
|
|
3092
|
+
if (persisted?.userToken && persisted?.hubUrl) {
|
|
3093
|
+
await (0, hub_1.hubRequestJson)(persisted.hubUrl, persisted.userToken, "/api/v1/hub/leave", { method: "POST" });
|
|
3094
|
+
this.log.info("Notified Hub of voluntary leave (had token)");
|
|
3095
|
+
return;
|
|
3096
|
+
}
|
|
3097
|
+
const hub = this.resolveHubConnection();
|
|
3098
|
+
if (hub?.userToken) {
|
|
3099
|
+
await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/leave", { method: "POST" });
|
|
3100
|
+
this.log.info("Notified Hub of voluntary leave (resolved connection)");
|
|
3101
|
+
return;
|
|
3102
|
+
}
|
|
3103
|
+
const hubUrl = persisted?.hubUrl || (sharing?.client?.hubAddress ? (0, hub_1.normalizeHubUrl)(sharing.client.hubAddress) : null);
|
|
3104
|
+
const userId = persisted?.userId;
|
|
3105
|
+
const teamToken = sharing?.client?.teamToken;
|
|
3106
|
+
if (hubUrl && userId && teamToken) {
|
|
3107
|
+
const withdrawUrl = `${(0, hub_1.normalizeHubUrl)(hubUrl)}/api/v1/hub/withdraw-pending`;
|
|
3108
|
+
await fetch(withdrawUrl, {
|
|
3109
|
+
method: "POST",
|
|
3110
|
+
headers: { "content-type": "application/json" },
|
|
3111
|
+
body: JSON.stringify({ teamToken, userId }),
|
|
3112
|
+
});
|
|
3113
|
+
this.log.info("Withdrew pending application from Hub");
|
|
3114
|
+
return;
|
|
3115
|
+
}
|
|
3116
|
+
this.log.info("No hub connection to clean up (no token, no pending)");
|
|
3117
|
+
}
|
|
3118
|
+
catch (e) {
|
|
3119
|
+
this.log.warn(`Failed to withdraw/leave Hub: ${e}`);
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
2889
3122
|
async notifyHubShutdown() {
|
|
2890
3123
|
try {
|
|
2891
3124
|
const sharing = this.ctx?.config.sharing;
|
|
2892
3125
|
if (!sharing || sharing.role !== "hub")
|
|
2893
3126
|
return;
|
|
2894
|
-
const hubPort =
|
|
3127
|
+
const hubPort = this.getHubPort();
|
|
2895
3128
|
const authPath = node_path_1.default.join(this.dataDir, "hub-auth.json");
|
|
2896
3129
|
let adminToken;
|
|
2897
3130
|
try {
|
|
@@ -2956,10 +3189,10 @@ class ViewerServer {
|
|
|
2956
3189
|
}
|
|
2957
3190
|
}
|
|
2958
3191
|
else {
|
|
2959
|
-
const
|
|
2960
|
-
if (
|
|
3192
|
+
const persistedConn = this.store.getClientHubConnection();
|
|
3193
|
+
if (persistedConn) {
|
|
2961
3194
|
this.store.setClientHubConnection({
|
|
2962
|
-
...
|
|
3195
|
+
...persistedConn,
|
|
2963
3196
|
username: result.username,
|
|
2964
3197
|
userToken: result.userToken,
|
|
2965
3198
|
});
|
|
@@ -2986,12 +3219,17 @@ class ViewerServer {
|
|
|
2986
3219
|
return;
|
|
2987
3220
|
}
|
|
2988
3221
|
try {
|
|
2989
|
-
const
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
3222
|
+
const sharing = this.ctx?.config?.sharing;
|
|
3223
|
+
if (sharing?.enabled && sharing.role === "hub") {
|
|
3224
|
+
const selfHubPort = this.getHubPort();
|
|
3225
|
+
const localIPs = this.getLocalIPs();
|
|
3226
|
+
localIPs.push("127.0.0.1", "localhost", "0.0.0.0");
|
|
3227
|
+
const parsed = new URL(hubUrl.startsWith("http") ? hubUrl : `http://${hubUrl}`);
|
|
3228
|
+
const targetPort = parsed.port || (parsed.protocol === "https:" ? "443" : "80");
|
|
3229
|
+
if (localIPs.includes(parsed.hostname) && targetPort === String(selfHubPort)) {
|
|
3230
|
+
this.jsonResponse(res, { ok: false, error: "cannot_join_self" });
|
|
3231
|
+
return;
|
|
3232
|
+
}
|
|
2995
3233
|
}
|
|
2996
3234
|
}
|
|
2997
3235
|
catch { }
|
|
@@ -3249,10 +3487,14 @@ class ViewerServer {
|
|
|
3249
3487
|
catch { }
|
|
3250
3488
|
this.log.info(`update-install: success! Updated to ${newVersion}`);
|
|
3251
3489
|
this.jsonResponse(res, { ok: true, version: newVersion });
|
|
3252
|
-
// Trigger Gateway restart after response is sent
|
|
3253
3490
|
setTimeout(() => {
|
|
3254
|
-
this.log.info(`update-install: triggering gateway restart...`);
|
|
3255
|
-
|
|
3491
|
+
this.log.info(`update-install: triggering gateway restart via SIGUSR1...`);
|
|
3492
|
+
try {
|
|
3493
|
+
process.kill(process.pid, "SIGUSR1");
|
|
3494
|
+
}
|
|
3495
|
+
catch (sig) {
|
|
3496
|
+
this.log.warn(`SIGUSR1 failed: ${sig}`);
|
|
3497
|
+
}
|
|
3256
3498
|
}, 500);
|
|
3257
3499
|
});
|
|
3258
3500
|
});
|
|
@@ -3428,7 +3670,7 @@ class ViewerServer {
|
|
|
3428
3670
|
try {
|
|
3429
3671
|
const ocHome = this.getOpenClawHome();
|
|
3430
3672
|
const memoryDir = node_path_1.default.join(ocHome, "memory");
|
|
3431
|
-
const
|
|
3673
|
+
const agentsDir = node_path_1.default.join(ocHome, "agents");
|
|
3432
3674
|
const sqliteFiles = [];
|
|
3433
3675
|
if (node_fs_1.default.existsSync(memoryDir)) {
|
|
3434
3676
|
for (const f of node_fs_1.default.readdirSync(memoryDir)) {
|
|
@@ -3446,38 +3688,45 @@ class ViewerServer {
|
|
|
3446
3688
|
}
|
|
3447
3689
|
let sessionCount = 0;
|
|
3448
3690
|
let messageCount = 0;
|
|
3449
|
-
if (node_fs_1.default.existsSync(
|
|
3450
|
-
const
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
txt =
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3691
|
+
if (node_fs_1.default.existsSync(agentsDir)) {
|
|
3692
|
+
for (const entry of node_fs_1.default.readdirSync(agentsDir, { withFileTypes: true })) {
|
|
3693
|
+
if (!entry.isDirectory())
|
|
3694
|
+
continue;
|
|
3695
|
+
const sessDir = node_path_1.default.join(agentsDir, entry.name, "sessions");
|
|
3696
|
+
if (!node_fs_1.default.existsSync(sessDir))
|
|
3697
|
+
continue;
|
|
3698
|
+
const jsonlFiles = node_fs_1.default.readdirSync(sessDir).filter(f => f.includes(".jsonl"));
|
|
3699
|
+
sessionCount += jsonlFiles.length;
|
|
3700
|
+
for (const f of jsonlFiles) {
|
|
3701
|
+
try {
|
|
3702
|
+
const content = node_fs_1.default.readFileSync(node_path_1.default.join(sessDir, f), "utf-8");
|
|
3703
|
+
const lines = content.split("\n").filter(l => l.trim());
|
|
3704
|
+
for (const line of lines) {
|
|
3705
|
+
try {
|
|
3706
|
+
const obj = JSON.parse(line);
|
|
3707
|
+
if (obj.type === "message") {
|
|
3708
|
+
const role = obj.message?.role ?? obj.role;
|
|
3709
|
+
if (role === "user" || role === "assistant") {
|
|
3710
|
+
const mc = obj.message?.content ?? obj.content;
|
|
3711
|
+
let txt = "";
|
|
3712
|
+
if (typeof mc === "string")
|
|
3713
|
+
txt = mc;
|
|
3714
|
+
else if (Array.isArray(mc))
|
|
3715
|
+
txt = mc.filter((p) => p.type === "text" && p.text).map((p) => p.text).join("\n");
|
|
3716
|
+
else
|
|
3717
|
+
txt = JSON.stringify(mc);
|
|
3718
|
+
if (role === "user")
|
|
3719
|
+
txt = (0, capture_1.stripInboundMetadata)(txt);
|
|
3720
|
+
if (txt && txt.length >= 10)
|
|
3721
|
+
messageCount++;
|
|
3722
|
+
}
|
|
3474
3723
|
}
|
|
3475
3724
|
}
|
|
3725
|
+
catch { /* skip bad lines */ }
|
|
3476
3726
|
}
|
|
3477
|
-
catch { /* skip bad lines */ }
|
|
3478
3727
|
}
|
|
3728
|
+
catch { /* skip unreadable */ }
|
|
3479
3729
|
}
|
|
3480
|
-
catch { /* skip unreadable */ }
|
|
3481
3730
|
}
|
|
3482
3731
|
}
|
|
3483
3732
|
const cfgPath = this.getOpenClawConfigPath();
|