@memtensor/memos-local-openclaw-plugin 1.0.4-beta.8 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +7 -0
- package/README.md +94 -27
- package/dist/capture/index.js +3 -1
- package/dist/capture/index.js.map +1 -1
- package/dist/client/connector.d.ts +5 -0
- package/dist/client/connector.d.ts.map +1 -1
- package/dist/client/connector.js +132 -10
- package/dist/client/connector.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -1
- package/dist/config.js.map +1 -1
- package/dist/hub/server.d.ts +2 -0
- package/dist/hub/server.d.ts.map +1 -1
- package/dist/hub/server.js +251 -38
- package/dist/hub/server.js.map +1 -1
- package/dist/hub/user-manager.d.ts +9 -0
- package/dist/hub/user-manager.d.ts.map +1 -1
- package/dist/hub/user-manager.js +26 -2
- package/dist/hub/user-manager.js.map +1 -1
- package/dist/ingest/chunker.d.ts +2 -1
- package/dist/ingest/chunker.d.ts.map +1 -1
- package/dist/ingest/chunker.js +14 -10
- package/dist/ingest/chunker.js.map +1 -1
- package/dist/ingest/providers/index.js +2 -2
- package/dist/ingest/providers/index.js.map +1 -1
- package/dist/recall/engine.d.ts.map +1 -1
- package/dist/recall/engine.js +96 -1
- package/dist/recall/engine.js.map +1 -1
- package/dist/shared/llm-call.d.ts.map +1 -1
- package/dist/shared/llm-call.js +2 -1
- package/dist/shared/llm-call.js.map +1 -1
- package/dist/sharing/types.d.ts +1 -1
- package/dist/sharing/types.d.ts.map +1 -1
- package/dist/skill/evolver.d.ts +2 -0
- package/dist/skill/evolver.d.ts.map +1 -1
- package/dist/skill/evolver.js +56 -5
- package/dist/skill/evolver.js.map +1 -1
- package/dist/skill/generator.d.ts +2 -0
- package/dist/skill/generator.d.ts.map +1 -1
- package/dist/skill/generator.js +45 -3
- package/dist/skill/generator.js.map +1 -1
- package/dist/skill/installer.d.ts +26 -0
- package/dist/skill/installer.d.ts.map +1 -1
- package/dist/skill/installer.js +80 -4
- package/dist/skill/installer.js.map +1 -1
- package/dist/skill/upgrader.d.ts +2 -0
- package/dist/skill/upgrader.d.ts.map +1 -1
- package/dist/skill/upgrader.js +139 -1
- package/dist/skill/upgrader.js.map +1 -1
- package/dist/skill/validator.d.ts +3 -0
- package/dist/skill/validator.d.ts.map +1 -1
- package/dist/skill/validator.js +75 -0
- package/dist/skill/validator.js.map +1 -1
- package/dist/storage/sqlite.d.ts +58 -0
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +295 -35
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +27 -8
- package/dist/telemetry.js.map +1 -1
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -1
- package/dist/viewer/html.d.ts.map +1 -1
- package/dist/viewer/html.js +796 -289
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +11 -0
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +456 -92
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +411 -52
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
- package/prebuilds/darwin-arm64/better_sqlite3.node +0 -0
- package/prebuilds/darwin-x64/better_sqlite3.node +0 -0
- package/prebuilds/linux-x64/better_sqlite3.node +0 -0
- package/prebuilds/win32-x64/better_sqlite3.node +0 -0
- package/src/capture/index.ts +4 -1
- package/src/client/connector.ts +136 -10
- package/src/config.ts +2 -1
- package/src/hub/server.ts +246 -38
- package/src/hub/user-manager.ts +42 -6
- package/src/ingest/chunker.ts +19 -13
- package/src/ingest/providers/index.ts +2 -2
- package/src/recall/engine.ts +89 -1
- package/src/shared/llm-call.ts +2 -1
- package/src/sharing/types.ts +1 -1
- package/src/skill/evolver.ts +58 -6
- package/src/skill/generator.ts +44 -5
- package/src/skill/installer.ts +107 -4
- package/src/skill/upgrader.ts +139 -1
- package/src/skill/validator.ts +79 -0
- package/src/storage/sqlite.ts +326 -40
- package/src/telemetry.ts +27 -9
- package/src/types.ts +11 -0
- package/src/viewer/html.ts +796 -289
- package/src/viewer/server.ts +430 -89
- package/telemetry.credentials.json +5 -0
package/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";
|
|
@@ -758,6 +785,13 @@ class ViewerServer {
|
|
|
758
785
|
owners = ownerRows.map((o) => o.owner);
|
|
759
786
|
}
|
|
760
787
|
catch { /* column may not exist yet */ }
|
|
788
|
+
let currentAgentOwner = "agent:main";
|
|
789
|
+
try {
|
|
790
|
+
const latest = db.prepare("SELECT owner FROM chunks WHERE owner IS NOT NULL AND owner LIKE 'agent:%' ORDER BY created_at DESC LIMIT 1").get();
|
|
791
|
+
if (latest?.owner)
|
|
792
|
+
currentAgentOwner = latest.owner;
|
|
793
|
+
}
|
|
794
|
+
catch { /* best-effort */ }
|
|
761
795
|
this.jsonResponse(res, {
|
|
762
796
|
totalMemories: total.count, totalSessions: sessions.count, totalEmbeddings: embCount,
|
|
763
797
|
totalSkills: skillCount,
|
|
@@ -766,6 +800,7 @@ class ViewerServer {
|
|
|
766
800
|
timeRange: { earliest: timeRange.earliest, latest: timeRange.latest },
|
|
767
801
|
sessions: sessionList,
|
|
768
802
|
owners,
|
|
803
|
+
currentAgentOwner,
|
|
769
804
|
});
|
|
770
805
|
}
|
|
771
806
|
catch (e) {
|
|
@@ -1154,7 +1189,7 @@ class ViewerServer {
|
|
|
1154
1189
|
}
|
|
1155
1190
|
});
|
|
1156
1191
|
}
|
|
1157
|
-
handleSkillDelete(res, urlPath) {
|
|
1192
|
+
async handleSkillDelete(res, urlPath) {
|
|
1158
1193
|
const skillId = urlPath.replace("/api/skill/", "");
|
|
1159
1194
|
const skill = this.store.getSkill(skillId);
|
|
1160
1195
|
if (!skill) {
|
|
@@ -1162,7 +1197,18 @@ class ViewerServer {
|
|
|
1162
1197
|
res.end(JSON.stringify({ error: "Skill not found" }));
|
|
1163
1198
|
return;
|
|
1164
1199
|
}
|
|
1165
|
-
|
|
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 (_) { }
|
|
1166
1212
|
try {
|
|
1167
1213
|
if (skill.dirPath && node_fs_1.default.existsSync(skill.dirPath)) {
|
|
1168
1214
|
node_fs_1.default.rmSync(skill.dirPath, { recursive: true, force: true });
|
|
@@ -1331,8 +1377,6 @@ class ViewerServer {
|
|
|
1331
1377
|
}
|
|
1332
1378
|
let hubSynced = false;
|
|
1333
1379
|
if (scope === "team") {
|
|
1334
|
-
if (!isLocalShared)
|
|
1335
|
-
this.store.markMemorySharedLocally(chunkId);
|
|
1336
1380
|
if (!isTeamShared) {
|
|
1337
1381
|
const hubClient = await this.resolveHubClientAware();
|
|
1338
1382
|
const refreshedChunk = db.prepare("SELECT * FROM chunks WHERE id = ?").get(chunkId);
|
|
@@ -1340,18 +1384,29 @@ class ViewerServer {
|
|
|
1340
1384
|
method: "POST",
|
|
1341
1385
|
body: JSON.stringify({ memory: { sourceChunkId: refreshedChunk.id, role: refreshedChunk.role, content: refreshedChunk.content, summary: refreshedChunk.summary, kind: refreshedChunk.kind, groupId: null, visibility: "public" } }),
|
|
1342
1386
|
});
|
|
1343
|
-
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) {
|
|
1344
1392
|
const existing = this.store.getHubMemoryBySource(hubClient.userId, chunkId);
|
|
1345
1393
|
this.store.upsertHubMemory({
|
|
1346
|
-
id:
|
|
1394
|
+
id: memoryId || existing?.id || node_crypto_1.default.randomUUID(),
|
|
1347
1395
|
sourceChunkId: chunkId, sourceUserId: hubClient.userId,
|
|
1348
1396
|
role: refreshedChunk.role, content: refreshedChunk.content, summary: refreshedChunk.summary ?? "",
|
|
1349
1397
|
kind: refreshedChunk.kind, groupId: null, visibility: "public",
|
|
1350
1398
|
createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(),
|
|
1351
1399
|
});
|
|
1352
1400
|
}
|
|
1401
|
+
else if (hubClient.userId) {
|
|
1402
|
+
this.store.upsertTeamSharedChunk(chunkId, { hubMemoryId: memoryId, visibility: "public", groupId: null });
|
|
1403
|
+
}
|
|
1353
1404
|
hubSynced = true;
|
|
1354
1405
|
}
|
|
1406
|
+
else {
|
|
1407
|
+
if (!isLocalShared)
|
|
1408
|
+
this.store.markMemorySharedLocally(chunkId);
|
|
1409
|
+
}
|
|
1355
1410
|
}
|
|
1356
1411
|
else if (scope === "local") {
|
|
1357
1412
|
if (isTeamShared) {
|
|
@@ -1362,6 +1417,7 @@ class ViewerServer {
|
|
|
1362
1417
|
});
|
|
1363
1418
|
if (hubClient.userId)
|
|
1364
1419
|
this.store.deleteHubMemoryBySource(hubClient.userId, chunkId);
|
|
1420
|
+
this.store.deleteTeamSharedChunk(chunkId);
|
|
1365
1421
|
hubSynced = true;
|
|
1366
1422
|
}
|
|
1367
1423
|
catch (err) {
|
|
@@ -1380,6 +1436,7 @@ class ViewerServer {
|
|
|
1380
1436
|
});
|
|
1381
1437
|
if (hubClient.userId)
|
|
1382
1438
|
this.store.deleteHubMemoryBySource(hubClient.userId, chunkId);
|
|
1439
|
+
this.store.deleteTeamSharedChunk(chunkId);
|
|
1383
1440
|
hubSynced = true;
|
|
1384
1441
|
}
|
|
1385
1442
|
catch (err) {
|
|
@@ -1419,14 +1476,6 @@ class ViewerServer {
|
|
|
1419
1476
|
return this.jsonResponse(res, { ok: true, scope, changed: false });
|
|
1420
1477
|
}
|
|
1421
1478
|
let hubSynced = false;
|
|
1422
|
-
if (scope === "local" || scope === "team") {
|
|
1423
|
-
if (!isLocalShared) {
|
|
1424
|
-
const originalOwner = task.owner;
|
|
1425
|
-
const db = this.store.db;
|
|
1426
|
-
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());
|
|
1427
|
-
db.prepare("UPDATE tasks SET owner = 'public' WHERE id = ?").run(taskId);
|
|
1428
|
-
}
|
|
1429
|
-
}
|
|
1430
1479
|
if (scope === "team") {
|
|
1431
1480
|
if (!isTeamShared) {
|
|
1432
1481
|
const chunks = this.store.getChunksByTask(taskId);
|
|
@@ -1450,6 +1499,20 @@ class ViewerServer {
|
|
|
1450
1499
|
}
|
|
1451
1500
|
hubSynced = true;
|
|
1452
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
|
+
}
|
|
1453
1516
|
}
|
|
1454
1517
|
if (scope === "local" && isTeamShared) {
|
|
1455
1518
|
try {
|
|
@@ -1520,10 +1583,6 @@ class ViewerServer {
|
|
|
1520
1583
|
return this.jsonResponse(res, { ok: true, scope, changed: false });
|
|
1521
1584
|
}
|
|
1522
1585
|
let hubSynced = false;
|
|
1523
|
-
if (scope === "local" || scope === "team") {
|
|
1524
|
-
if (!isLocalShared)
|
|
1525
|
-
this.store.setSkillVisibility(skillId, "public");
|
|
1526
|
-
}
|
|
1527
1586
|
if (scope === "team") {
|
|
1528
1587
|
if (!isTeamShared) {
|
|
1529
1588
|
const bundle = (0, skill_sync_1.buildSkillBundleForHub)(this.store, skillId);
|
|
@@ -1545,6 +1604,12 @@ class ViewerServer {
|
|
|
1545
1604
|
}
|
|
1546
1605
|
hubSynced = true;
|
|
1547
1606
|
}
|
|
1607
|
+
if (!isLocalShared)
|
|
1608
|
+
this.store.setSkillVisibility(skillId, "public");
|
|
1609
|
+
}
|
|
1610
|
+
if (scope === "local") {
|
|
1611
|
+
if (!isLocalShared)
|
|
1612
|
+
this.store.setSkillVisibility(skillId, "public");
|
|
1548
1613
|
}
|
|
1549
1614
|
if (scope === "local" && isTeamShared) {
|
|
1550
1615
|
try {
|
|
@@ -1587,7 +1652,18 @@ class ViewerServer {
|
|
|
1587
1652
|
}
|
|
1588
1653
|
getHubMemoryForChunk(chunkId) {
|
|
1589
1654
|
const db = this.store.db;
|
|
1590
|
-
|
|
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;
|
|
1591
1667
|
}
|
|
1592
1668
|
getHubTaskForLocal(taskId) {
|
|
1593
1669
|
const db = this.store.db;
|
|
@@ -1633,6 +1709,8 @@ class ViewerServer {
|
|
|
1633
1709
|
// ─── Helpers ───
|
|
1634
1710
|
// ─── Config API ───
|
|
1635
1711
|
getOpenClawConfigPath() {
|
|
1712
|
+
if (process.env.OPENCLAW_CONFIG_PATH)
|
|
1713
|
+
return process.env.OPENCLAW_CONFIG_PATH;
|
|
1636
1714
|
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
1637
1715
|
const ocHome = process.env.OPENCLAW_STATE_DIR || node_path_1.default.join(home, ".openclaw");
|
|
1638
1716
|
return node_path_1.default.join(ocHome, "openclaw.json");
|
|
@@ -1715,7 +1793,18 @@ class ViewerServer {
|
|
|
1715
1793
|
base.connection.apiVersion = info?.apiVersion ?? null;
|
|
1716
1794
|
}
|
|
1717
1795
|
catch { /* ignore */ }
|
|
1718
|
-
|
|
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 });
|
|
1719
1808
|
return;
|
|
1720
1809
|
}
|
|
1721
1810
|
const hasPendingConnection = Boolean(persisted?.hubUrl && persisted?.userId && !persisted?.userToken);
|
|
@@ -1732,6 +1821,9 @@ class ViewerServer {
|
|
|
1732
1821
|
if (status.user?.status === "rejected") {
|
|
1733
1822
|
output.connection.rejected = true;
|
|
1734
1823
|
}
|
|
1824
|
+
if (status.user?.status === "removed") {
|
|
1825
|
+
output.connection.removed = true;
|
|
1826
|
+
}
|
|
1735
1827
|
if (status.connected && status.hubUrl) {
|
|
1736
1828
|
try {
|
|
1737
1829
|
const info = await fetch(`${status.hubUrl}/api/v1/hub/info`).then((r) => (r.ok ? r.json() : null)).catch(() => null);
|
|
@@ -1865,7 +1957,16 @@ class ViewerServer {
|
|
|
1865
1957
|
this.jsonResponse(res, { ok: true, result });
|
|
1866
1958
|
}
|
|
1867
1959
|
catch (err) {
|
|
1868
|
-
|
|
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
|
+
}
|
|
1869
1970
|
}
|
|
1870
1971
|
});
|
|
1871
1972
|
}
|
|
@@ -1888,10 +1989,13 @@ class ViewerServer {
|
|
|
1888
1989
|
const nickname = sharing.client?.nickname;
|
|
1889
1990
|
const username = nickname || os.userInfo().username || "user";
|
|
1890
1991
|
const hostname = os.hostname() || "unknown";
|
|
1992
|
+
const persisted = this.store.getClientHubConnection();
|
|
1993
|
+
const existingIdentityKey = persisted?.identityKey || "";
|
|
1891
1994
|
const result = await (0, hub_1.hubRequestJson)(hubUrl, "", "/api/v1/hub/join", {
|
|
1892
1995
|
method: "POST",
|
|
1893
|
-
body: JSON.stringify({ teamToken, username, deviceName: hostname }),
|
|
1996
|
+
body: JSON.stringify({ teamToken, username, deviceName: hostname, reapply: true, identityKey: existingIdentityKey }),
|
|
1894
1997
|
});
|
|
1998
|
+
const returnedIdentityKey = String(result.identityKey || existingIdentityKey || "");
|
|
1895
1999
|
this.store.setClientHubConnection({
|
|
1896
2000
|
hubUrl,
|
|
1897
2001
|
userId: String(result.userId || ""),
|
|
@@ -1899,6 +2003,8 @@ class ViewerServer {
|
|
|
1899
2003
|
userToken: result.userToken || "",
|
|
1900
2004
|
role: "member",
|
|
1901
2005
|
connectedAt: Date.now(),
|
|
2006
|
+
identityKey: returnedIdentityKey,
|
|
2007
|
+
lastKnownStatus: result.status || "",
|
|
1902
2008
|
});
|
|
1903
2009
|
this.jsonResponse(res, { ok: true, status: result.status || "pending" });
|
|
1904
2010
|
}
|
|
@@ -2194,14 +2300,14 @@ class ViewerServer {
|
|
|
2194
2300
|
},
|
|
2195
2301
|
}),
|
|
2196
2302
|
});
|
|
2197
|
-
const
|
|
2198
|
-
if (
|
|
2303
|
+
const mid = String(response?.memoryId ?? "");
|
|
2304
|
+
if (hubClient.userId && this.ctx?.config?.sharing?.role === "hub") {
|
|
2199
2305
|
const now = Date.now();
|
|
2200
|
-
const existing = this.store.getHubMemoryBySource(
|
|
2306
|
+
const existing = this.store.getHubMemoryBySource(hubClient.userId, chunk.id);
|
|
2201
2307
|
this.store.upsertHubMemory({
|
|
2202
|
-
id:
|
|
2308
|
+
id: mid || existing?.id || node_crypto_1.default.randomUUID(),
|
|
2203
2309
|
sourceChunkId: chunk.id,
|
|
2204
|
-
sourceUserId:
|
|
2310
|
+
sourceUserId: hubClient.userId,
|
|
2205
2311
|
role: chunk.role,
|
|
2206
2312
|
content: chunk.content,
|
|
2207
2313
|
summary: chunk.summary ?? "",
|
|
@@ -2212,6 +2318,9 @@ class ViewerServer {
|
|
|
2212
2318
|
updatedAt: now,
|
|
2213
2319
|
});
|
|
2214
2320
|
}
|
|
2321
|
+
else if (hubClient.userId) {
|
|
2322
|
+
this.store.upsertTeamSharedChunk(chunk.id, { hubMemoryId: mid, visibility, groupId });
|
|
2323
|
+
}
|
|
2215
2324
|
this.jsonResponse(res, { ok: true, chunkId, visibility, response });
|
|
2216
2325
|
}
|
|
2217
2326
|
catch (err) {
|
|
@@ -2234,6 +2343,7 @@ class ViewerServer {
|
|
|
2234
2343
|
const hubUserId = hubClient.userId;
|
|
2235
2344
|
if (hubUserId)
|
|
2236
2345
|
this.store.deleteHubMemoryBySource(hubUserId, chunkId);
|
|
2346
|
+
this.store.deleteTeamSharedChunk(chunkId);
|
|
2237
2347
|
this.jsonResponse(res, { ok: true, chunkId });
|
|
2238
2348
|
}
|
|
2239
2349
|
catch (err) {
|
|
@@ -2336,7 +2446,7 @@ class ViewerServer {
|
|
|
2336
2446
|
// Hub 模式:连接自己,用 bootstrap admin token
|
|
2337
2447
|
const sharing = this.ctx.config.sharing;
|
|
2338
2448
|
if (sharing?.role === "hub") {
|
|
2339
|
-
const hubPort =
|
|
2449
|
+
const hubPort = this.getHubPort();
|
|
2340
2450
|
const hubUrl = `http://127.0.0.1:${hubPort}`;
|
|
2341
2451
|
try {
|
|
2342
2452
|
const authPath = node_path_1.default.join(this.dataDir, "hub-auth.json");
|
|
@@ -2578,6 +2688,32 @@ class ViewerServer {
|
|
|
2578
2688
|
}
|
|
2579
2689
|
});
|
|
2580
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
|
+
}
|
|
2581
2717
|
handleNotifSSE(req, res) {
|
|
2582
2718
|
res.writeHead(200, {
|
|
2583
2719
|
"Content-Type": "text/event-stream",
|
|
@@ -2589,6 +2725,8 @@ class ViewerServer {
|
|
|
2589
2725
|
this.notifSSEClients.push(res);
|
|
2590
2726
|
if (!this.notifPollTimer)
|
|
2591
2727
|
this.startNotifPoll();
|
|
2728
|
+
else
|
|
2729
|
+
this.notifPollImmediate();
|
|
2592
2730
|
req.on("close", () => {
|
|
2593
2731
|
this.notifSSEClients = this.notifSSEClients.filter((c) => c !== res);
|
|
2594
2732
|
if (this.notifSSEClients.length === 0)
|
|
@@ -2632,6 +2770,20 @@ class ViewerServer {
|
|
|
2632
2770
|
this.notifPollTimer = undefined;
|
|
2633
2771
|
}
|
|
2634
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
|
+
}
|
|
2635
2787
|
startHubHeartbeat() {
|
|
2636
2788
|
this.stopHubHeartbeat();
|
|
2637
2789
|
const sendHeartbeat = async () => {
|
|
@@ -2733,7 +2885,10 @@ class ViewerServer {
|
|
|
2733
2885
|
if (!entry.config)
|
|
2734
2886
|
entry.config = {};
|
|
2735
2887
|
const config = entry.config;
|
|
2736
|
-
const
|
|
2888
|
+
const oldSharing = config.sharing;
|
|
2889
|
+
const oldSharingRole = oldSharing?.role;
|
|
2890
|
+
const oldSharingEnabled = Boolean(oldSharing?.enabled);
|
|
2891
|
+
const oldClientHubAddress = String(oldSharing?.client?.hubAddress || "");
|
|
2737
2892
|
if (newCfg.embedding)
|
|
2738
2893
|
config.embedding = newCfg.embedding;
|
|
2739
2894
|
if (newCfg.summarizer)
|
|
@@ -2753,12 +2908,14 @@ class ViewerServer {
|
|
|
2753
2908
|
if (merged.role === "client" && merged.client) {
|
|
2754
2909
|
const clientCfg = merged.client;
|
|
2755
2910
|
const addr = String(clientCfg.hubAddress || "");
|
|
2756
|
-
if (addr) {
|
|
2911
|
+
if (addr && oldSharingRole === "hub" && oldSharingEnabled) {
|
|
2912
|
+
const selfHubPort = oldSharing?.hub?.port ?? 18800;
|
|
2757
2913
|
const localIPs = this.getLocalIPs();
|
|
2758
2914
|
localIPs.push("127.0.0.1", "localhost", "0.0.0.0");
|
|
2759
2915
|
try {
|
|
2760
2916
|
const u = new URL(addr.startsWith("http") ? addr : `http://${addr}`);
|
|
2761
|
-
|
|
2917
|
+
const targetPort = u.port || (u.protocol === "https:" ? "443" : "80");
|
|
2918
|
+
if (localIPs.includes(u.hostname) && targetPort === String(selfHubPort)) {
|
|
2762
2919
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2763
2920
|
res.end(JSON.stringify({ error: "cannot_join_self" }));
|
|
2764
2921
|
return;
|
|
@@ -2767,16 +2924,40 @@ class ViewerServer {
|
|
|
2767
2924
|
catch { }
|
|
2768
2925
|
}
|
|
2769
2926
|
}
|
|
2770
|
-
// When switching away from client mode, notify Hub that we're leaving
|
|
2771
2927
|
const newRole = merged.role;
|
|
2772
|
-
|
|
2773
|
-
|
|
2928
|
+
const newEnabled = Boolean(merged.enabled);
|
|
2929
|
+
// Detect disabling sharing or switching away from hub mode
|
|
2930
|
+
const wasHub = oldSharingEnabled && oldSharingRole === "hub";
|
|
2931
|
+
const isHub = newEnabled && newRole === "hub";
|
|
2932
|
+
if (wasHub && !isHub) {
|
|
2933
|
+
await this.notifyHubShutdown();
|
|
2934
|
+
this.stopHubHeartbeat();
|
|
2935
|
+
this.log.info("Hub shutting down: notified connected clients");
|
|
2936
|
+
}
|
|
2937
|
+
// Detect disabling sharing or switching away from client mode
|
|
2938
|
+
const wasClient = oldSharingEnabled && oldSharingRole === "client";
|
|
2939
|
+
const isClient = newEnabled && newRole === "client";
|
|
2940
|
+
if (wasClient && !isClient) {
|
|
2941
|
+
await this.withdrawOrLeaveHub();
|
|
2942
|
+
this.store.clearClientHubConnection();
|
|
2943
|
+
this.log.info("Client hub connection cleared (sharing disabled or role changed)");
|
|
2944
|
+
}
|
|
2945
|
+
if (wasClient && isClient) {
|
|
2946
|
+
const newClientAddr = String(merged.client?.hubAddress || "");
|
|
2947
|
+
if (newClientAddr && oldClientHubAddress && (0, hub_1.normalizeHubUrl)(newClientAddr) !== (0, hub_1.normalizeHubUrl)(oldClientHubAddress)) {
|
|
2948
|
+
this.notifyHubLeave();
|
|
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");
|
|
2954
|
+
}
|
|
2774
2955
|
}
|
|
2775
2956
|
if (merged.role === "hub") {
|
|
2776
2957
|
merged.client = { hubAddress: "", userToken: "", teamToken: "" };
|
|
2777
2958
|
}
|
|
2778
2959
|
else if (merged.role === "client") {
|
|
2779
|
-
merged.hub = {
|
|
2960
|
+
merged.hub = { teamName: "", teamToken: "" };
|
|
2780
2961
|
}
|
|
2781
2962
|
config.sharing = merged;
|
|
2782
2963
|
}
|
|
@@ -2784,7 +2965,29 @@ class ViewerServer {
|
|
|
2784
2965
|
node_fs_1.default.writeFileSync(cfgPath, JSON.stringify(raw, null, 2), "utf-8");
|
|
2785
2966
|
this.log.info("Plugin config updated via Viewer");
|
|
2786
2967
|
this.stopHubHeartbeat();
|
|
2787
|
-
|
|
2968
|
+
// When switching to client mode or re-enabling sharing as client, send join request
|
|
2969
|
+
const finalSharing = config.sharing;
|
|
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
|
+
}
|
|
2980
|
+
}
|
|
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);
|
|
2788
2991
|
}
|
|
2789
2992
|
catch (e) {
|
|
2790
2993
|
this.log.warn(`handleSaveConfig error: ${e}`);
|
|
@@ -2793,6 +2996,77 @@ class ViewerServer {
|
|
|
2793
2996
|
}
|
|
2794
2997
|
});
|
|
2795
2998
|
}
|
|
2999
|
+
async autoJoinOnSave(sharing) {
|
|
3000
|
+
const clientCfg = sharing.client;
|
|
3001
|
+
const hubAddress = String(clientCfg?.hubAddress || "");
|
|
3002
|
+
const teamToken = String(clientCfg?.teamToken || "");
|
|
3003
|
+
if (!hubAddress || !teamToken)
|
|
3004
|
+
return undefined;
|
|
3005
|
+
const hubUrl = (0, hub_1.normalizeHubUrl)(hubAddress);
|
|
3006
|
+
const os = await Promise.resolve().then(() => __importStar(require("os")));
|
|
3007
|
+
const nickname = String(clientCfg?.nickname || "");
|
|
3008
|
+
const username = nickname || os.userInfo().username || "user";
|
|
3009
|
+
const hostname = os.hostname() || "unknown";
|
|
3010
|
+
const persisted = this.store.getClientHubConnection();
|
|
3011
|
+
const existingIdentityKey = persisted?.identityKey || "";
|
|
3012
|
+
const result = await (0, hub_1.hubRequestJson)(hubUrl, "", "/api/v1/hub/join", {
|
|
3013
|
+
method: "POST",
|
|
3014
|
+
body: JSON.stringify({ teamToken, username, deviceName: hostname, identityKey: existingIdentityKey }),
|
|
3015
|
+
});
|
|
3016
|
+
const returnedIdentityKey = String(result.identityKey || existingIdentityKey || "");
|
|
3017
|
+
this.store.setClientHubConnection({
|
|
3018
|
+
hubUrl,
|
|
3019
|
+
userId: String(result.userId || ""),
|
|
3020
|
+
username,
|
|
3021
|
+
userToken: result.userToken || "",
|
|
3022
|
+
role: "member",
|
|
3023
|
+
connectedAt: Date.now(),
|
|
3024
|
+
identityKey: returnedIdentityKey,
|
|
3025
|
+
lastKnownStatus: result.status || "",
|
|
3026
|
+
});
|
|
3027
|
+
this.log.info(`Auto-join on save: status=${result.status}, userId=${result.userId}`);
|
|
3028
|
+
if (result.userToken) {
|
|
3029
|
+
this.startHubHeartbeat();
|
|
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
|
+
});
|
|
3069
|
+
}
|
|
2796
3070
|
async notifyHubLeave() {
|
|
2797
3071
|
try {
|
|
2798
3072
|
const hub = this.resolveHubConnection();
|
|
@@ -2811,6 +3085,80 @@ class ViewerServer {
|
|
|
2811
3085
|
this.log.warn(`Failed to notify Hub of leave: ${e}`);
|
|
2812
3086
|
}
|
|
2813
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
|
+
}
|
|
3122
|
+
async notifyHubShutdown() {
|
|
3123
|
+
try {
|
|
3124
|
+
const sharing = this.ctx?.config.sharing;
|
|
3125
|
+
if (!sharing || sharing.role !== "hub")
|
|
3126
|
+
return;
|
|
3127
|
+
const hubPort = this.getHubPort();
|
|
3128
|
+
const authPath = node_path_1.default.join(this.dataDir, "hub-auth.json");
|
|
3129
|
+
let adminToken;
|
|
3130
|
+
try {
|
|
3131
|
+
const authData = JSON.parse(node_fs_1.default.readFileSync(authPath, "utf8"));
|
|
3132
|
+
adminToken = authData?.bootstrapAdminToken;
|
|
3133
|
+
}
|
|
3134
|
+
catch {
|
|
3135
|
+
return;
|
|
3136
|
+
}
|
|
3137
|
+
if (!adminToken)
|
|
3138
|
+
return;
|
|
3139
|
+
const users = this.store.listHubUsers("active");
|
|
3140
|
+
const { v4: uuidv4 } = require("uuid");
|
|
3141
|
+
for (const u of users) {
|
|
3142
|
+
try {
|
|
3143
|
+
this.store.insertHubNotification({
|
|
3144
|
+
id: uuidv4(),
|
|
3145
|
+
userId: u.id,
|
|
3146
|
+
type: "hub_shutdown",
|
|
3147
|
+
resource: "hub",
|
|
3148
|
+
title: "Hub is shutting down",
|
|
3149
|
+
message: "The Hub server is shutting down. You may be disconnected.",
|
|
3150
|
+
});
|
|
3151
|
+
}
|
|
3152
|
+
catch (e) {
|
|
3153
|
+
this.log.warn(`Failed to insert shutdown notification for user ${u.id}: ${e}`);
|
|
3154
|
+
}
|
|
3155
|
+
}
|
|
3156
|
+
this.log.info(`Hub shutdown: notified ${users.length} approved user(s)`);
|
|
3157
|
+
}
|
|
3158
|
+
catch (e) {
|
|
3159
|
+
this.log.warn(`notifyHubShutdown error: ${e}`);
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
2814
3162
|
handleUpdateUsername(req, res) {
|
|
2815
3163
|
this.readBody(req, async (body) => {
|
|
2816
3164
|
if (!this.ctx)
|
|
@@ -2841,10 +3189,10 @@ class ViewerServer {
|
|
|
2841
3189
|
}
|
|
2842
3190
|
}
|
|
2843
3191
|
else {
|
|
2844
|
-
const
|
|
2845
|
-
if (
|
|
3192
|
+
const persistedConn = this.store.getClientHubConnection();
|
|
3193
|
+
if (persistedConn) {
|
|
2846
3194
|
this.store.setClientHubConnection({
|
|
2847
|
-
...
|
|
3195
|
+
...persistedConn,
|
|
2848
3196
|
username: result.username,
|
|
2849
3197
|
userToken: result.userToken,
|
|
2850
3198
|
});
|
|
@@ -2871,12 +3219,17 @@ class ViewerServer {
|
|
|
2871
3219
|
return;
|
|
2872
3220
|
}
|
|
2873
3221
|
try {
|
|
2874
|
-
const
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
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
|
+
}
|
|
2880
3233
|
}
|
|
2881
3234
|
}
|
|
2882
3235
|
catch { }
|
|
@@ -3134,10 +3487,14 @@ class ViewerServer {
|
|
|
3134
3487
|
catch { }
|
|
3135
3488
|
this.log.info(`update-install: success! Updated to ${newVersion}`);
|
|
3136
3489
|
this.jsonResponse(res, { ok: true, version: newVersion });
|
|
3137
|
-
// Trigger Gateway restart after response is sent
|
|
3138
3490
|
setTimeout(() => {
|
|
3139
|
-
this.log.info(`update-install: triggering gateway restart...`);
|
|
3140
|
-
|
|
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
|
+
}
|
|
3141
3498
|
}, 500);
|
|
3142
3499
|
});
|
|
3143
3500
|
});
|
|
@@ -3313,7 +3670,7 @@ class ViewerServer {
|
|
|
3313
3670
|
try {
|
|
3314
3671
|
const ocHome = this.getOpenClawHome();
|
|
3315
3672
|
const memoryDir = node_path_1.default.join(ocHome, "memory");
|
|
3316
|
-
const
|
|
3673
|
+
const agentsDir = node_path_1.default.join(ocHome, "agents");
|
|
3317
3674
|
const sqliteFiles = [];
|
|
3318
3675
|
if (node_fs_1.default.existsSync(memoryDir)) {
|
|
3319
3676
|
for (const f of node_fs_1.default.readdirSync(memoryDir)) {
|
|
@@ -3331,38 +3688,45 @@ class ViewerServer {
|
|
|
3331
3688
|
}
|
|
3332
3689
|
let sessionCount = 0;
|
|
3333
3690
|
let messageCount = 0;
|
|
3334
|
-
if (node_fs_1.default.existsSync(
|
|
3335
|
-
const
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
txt =
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
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
|
+
}
|
|
3359
3723
|
}
|
|
3360
3724
|
}
|
|
3725
|
+
catch { /* skip bad lines */ }
|
|
3361
3726
|
}
|
|
3362
|
-
catch { /* skip bad lines */ }
|
|
3363
3727
|
}
|
|
3728
|
+
catch { /* skip unreadable */ }
|
|
3364
3729
|
}
|
|
3365
|
-
catch { /* skip unreadable */ }
|
|
3366
3730
|
}
|
|
3367
3731
|
}
|
|
3368
3732
|
const cfgPath = this.getOpenClawConfigPath();
|