@memtensor/memos-local-openclaw-plugin 1.0.4-beta.1 → 1.0.4-beta.10
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 +24 -24
- package/dist/capture/index.d.ts +1 -1
- package/dist/capture/index.d.ts.map +1 -1
- package/dist/capture/index.js +34 -2
- package/dist/capture/index.js.map +1 -1
- package/dist/client/connector.d.ts +1 -2
- package/dist/client/connector.d.ts.map +1 -1
- package/dist/client/connector.js +93 -26
- package/dist/client/connector.js.map +1 -1
- package/dist/client/hub.d.ts.map +1 -1
- package/dist/client/hub.js +22 -0
- package/dist/client/hub.js.map +1 -1
- package/dist/client/skill-sync.d.ts +7 -0
- package/dist/client/skill-sync.d.ts.map +1 -1
- package/dist/client/skill-sync.js +10 -0
- package/dist/client/skill-sync.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +0 -2
- package/dist/config.js.map +1 -1
- package/dist/hub/server.d.ts +7 -0
- package/dist/hub/server.d.ts.map +1 -1
- package/dist/hub/server.js +277 -87
- package/dist/hub/server.js.map +1 -1
- package/dist/hub/user-manager.d.ts +2 -0
- package/dist/hub/user-manager.d.ts.map +1 -1
- package/dist/hub/user-manager.js +5 -1
- package/dist/hub/user-manager.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -2
- package/dist/index.js.map +1 -1
- package/dist/ingest/providers/index.d.ts.map +1 -1
- package/dist/ingest/providers/index.js +37 -6
- package/dist/ingest/providers/index.js.map +1 -1
- package/dist/recall/engine.d.ts.map +1 -1
- package/dist/recall/engine.js +91 -1
- package/dist/recall/engine.js.map +1 -1
- package/dist/shared/llm-call.d.ts +1 -0
- package/dist/shared/llm-call.d.ts.map +1 -1
- package/dist/shared/llm-call.js +82 -8
- 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 +3 -0
- package/dist/skill/evolver.js.map +1 -1
- package/dist/storage/ensure-binding.d.ts +12 -0
- package/dist/storage/ensure-binding.d.ts.map +1 -0
- package/dist/storage/ensure-binding.js +53 -0
- package/dist/storage/ensure-binding.js.map +1 -0
- package/dist/storage/sqlite.d.ts +74 -20
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +286 -118
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/telemetry.d.ts +12 -5
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +156 -40
- package/dist/telemetry.js.map +1 -1
- package/dist/tools/memory-search.d.ts +3 -1
- package/dist/tools/memory-search.d.ts.map +1 -1
- package/dist/tools/memory-search.js +3 -1
- package/dist/tools/memory-search.js.map +1 -1
- package/dist/types.d.ts +1 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/viewer/html.d.ts.map +1 -1
- package/dist/viewer/html.js +2660 -889
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +30 -8
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +965 -193
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +384 -43
- package/openclaw.plugin.json +1 -1
- package/package.json +3 -2
- package/scripts/postinstall.cjs +1 -1
- package/skill/memos-memory-guide/SKILL.md +64 -26
- package/src/capture/index.ts +37 -1
- package/src/client/connector.ts +91 -28
- package/src/client/hub.ts +18 -0
- package/src/client/skill-sync.ts +14 -0
- package/src/config.ts +0 -2
- package/src/hub/server.ts +259 -78
- package/src/hub/user-manager.ts +7 -3
- package/src/index.ts +10 -2
- package/src/ingest/providers/index.ts +41 -7
- package/src/recall/engine.ts +84 -1
- package/src/shared/llm-call.ts +97 -9
- package/src/sharing/types.ts +1 -1
- package/src/skill/evolver.ts +5 -0
- package/src/storage/ensure-binding.ts +52 -0
- package/src/storage/sqlite.ts +295 -144
- package/src/telemetry.ts +172 -41
- package/src/tools/memory-search.ts +2 -1
- package/src/types.ts +1 -2
- package/src/viewer/html.ts +2660 -889
- package/src/viewer/server.ts +888 -177
package/dist/viewer/server.js
CHANGED
|
@@ -91,6 +91,11 @@ class ViewerServer {
|
|
|
91
91
|
ppAbort = false;
|
|
92
92
|
ppState = { running: false, done: false, stopped: false, processed: 0, total: 0, tasksCreated: 0, skillsCreated: 0, errors: 0, skippedSessions: 0, totalSessions: 0 };
|
|
93
93
|
ppSSEClients = [];
|
|
94
|
+
notifSSEClients = [];
|
|
95
|
+
notifPollTimer;
|
|
96
|
+
lastKnownNotifCount = 0;
|
|
97
|
+
hubHeartbeatTimer;
|
|
98
|
+
static HUB_HEARTBEAT_INTERVAL_MS = 45_000;
|
|
94
99
|
constructor(opts) {
|
|
95
100
|
this.store = opts.store;
|
|
96
101
|
this.embedder = opts.embedder;
|
|
@@ -109,16 +114,17 @@ class ViewerServer {
|
|
|
109
114
|
this.server.on("error", (err) => {
|
|
110
115
|
if (err.code === "EADDRINUSE") {
|
|
111
116
|
this.log.warn(`Viewer port ${this.port} in use, trying ${this.port + 1}`);
|
|
112
|
-
this.server.listen(this.port + 1, "
|
|
117
|
+
this.server.listen(this.port + 1, "0.0.0.0");
|
|
113
118
|
}
|
|
114
119
|
else {
|
|
115
120
|
reject(err);
|
|
116
121
|
}
|
|
117
122
|
});
|
|
118
|
-
this.server.listen(this.port, "
|
|
123
|
+
this.server.listen(this.port, "0.0.0.0", () => {
|
|
119
124
|
const addr = this.server.address();
|
|
120
125
|
const actualPort = typeof addr === "object" && addr ? addr.port : this.port;
|
|
121
126
|
this.autoCleanupPolluted();
|
|
127
|
+
this.startHubHeartbeat();
|
|
122
128
|
resolve(`http://127.0.0.1:${actualPort}`);
|
|
123
129
|
});
|
|
124
130
|
});
|
|
@@ -141,6 +147,15 @@ class ViewerServer {
|
|
|
141
147
|
}
|
|
142
148
|
}
|
|
143
149
|
stop() {
|
|
150
|
+
this.stopHubHeartbeat();
|
|
151
|
+
this.stopNotifPoll();
|
|
152
|
+
for (const c of this.notifSSEClients) {
|
|
153
|
+
try {
|
|
154
|
+
c.end();
|
|
155
|
+
}
|
|
156
|
+
catch { }
|
|
157
|
+
}
|
|
158
|
+
this.notifSSEClients = [];
|
|
144
159
|
this.server?.close();
|
|
145
160
|
this.server = null;
|
|
146
161
|
}
|
|
@@ -228,6 +243,16 @@ class ViewerServer {
|
|
|
228
243
|
}
|
|
229
244
|
if (p === "/api/memories" && req.method === "GET")
|
|
230
245
|
this.serveMemories(res, url);
|
|
246
|
+
else if (p === "/api/memories/share-local" && req.method === "POST")
|
|
247
|
+
this.handleMemoryLocalShare(req, res);
|
|
248
|
+
else if (p === "/api/memories/unshare-local" && req.method === "POST")
|
|
249
|
+
this.handleMemoryLocalUnshare(req, res);
|
|
250
|
+
else if (p.match(/^\/api\/memory\/[^/]+\/scope$/) && req.method === "PUT")
|
|
251
|
+
this.handleMemoryScope(req, res, p);
|
|
252
|
+
else if (p.match(/^\/api\/task\/[^/]+\/scope$/) && req.method === "PUT")
|
|
253
|
+
this.handleTaskScope(req, res, p);
|
|
254
|
+
else if (p.match(/^\/api\/skill\/[^/]+\/scope$/) && req.method === "PUT")
|
|
255
|
+
this.handleSkillScope(req, res, p);
|
|
231
256
|
else if (p === "/api/stats")
|
|
232
257
|
this.serveStats(res, url);
|
|
233
258
|
else if (p === "/api/metrics")
|
|
@@ -282,6 +307,10 @@ class ViewerServer {
|
|
|
282
307
|
this.handleSharingApproveUser(req, res);
|
|
283
308
|
else if (p === "/api/sharing/reject-user" && req.method === "POST")
|
|
284
309
|
this.handleSharingRejectUser(req, res);
|
|
310
|
+
else if (p === "/api/sharing/remove-user" && req.method === "POST")
|
|
311
|
+
this.handleSharingRemoveUser(req, res);
|
|
312
|
+
else if (p === "/api/sharing/change-role" && req.method === "POST")
|
|
313
|
+
this.handleSharingChangeRole(req, res);
|
|
285
314
|
else if (p === "/api/sharing/retry-join" && req.method === "POST")
|
|
286
315
|
this.handleRetryJoin(req, res);
|
|
287
316
|
else if (p === "/api/sharing/search/memories" && req.method === "POST")
|
|
@@ -302,6 +331,8 @@ class ViewerServer {
|
|
|
302
331
|
this.handleSharingTaskUnshare(req, res);
|
|
303
332
|
else if (p === "/api/sharing/update-username" && req.method === "POST")
|
|
304
333
|
this.handleUpdateUsername(req, res);
|
|
334
|
+
else if (p === "/api/sharing/rename-user" && req.method === "POST")
|
|
335
|
+
this.handleAdminRenameUser(req, res);
|
|
305
336
|
else if (p === "/api/sharing/test-hub" && req.method === "POST")
|
|
306
337
|
this.handleTestHubConnection(req, res);
|
|
307
338
|
else if (p === "/api/sharing/memories/share" && req.method === "POST")
|
|
@@ -314,28 +345,26 @@ class ViewerServer {
|
|
|
314
345
|
this.handleSharingSkillShare(req, res);
|
|
315
346
|
else if (p === "/api/sharing/skills/unshare" && req.method === "POST")
|
|
316
347
|
this.handleSharingSkillUnshare(req, res);
|
|
317
|
-
else if (p === "/api/sharing/groups" && req.method === "GET")
|
|
318
|
-
this.serveSharingGroups(res);
|
|
319
|
-
else if (p === "/api/sharing/groups" && req.method === "POST")
|
|
320
|
-
this.handleSharingGroupCreate(req, res);
|
|
321
|
-
else if (p.match(/^\/api\/sharing\/groups\/[^/]+$/) && req.method === "PUT")
|
|
322
|
-
this.handleSharingGroupUpdate(req, res, p);
|
|
323
|
-
else if (p.match(/^\/api\/sharing\/groups\/[^/]+$/) && req.method === "DELETE")
|
|
324
|
-
this.handleSharingGroupDelete(res, p);
|
|
325
|
-
else if (p.match(/^\/api\/sharing\/groups\/[^/]+\/members$/) && req.method === "GET")
|
|
326
|
-
this.serveSharingGroupMembers(res, p);
|
|
327
|
-
else if (p.match(/^\/api\/sharing\/groups\/[^/]+\/members$/) && req.method === "POST")
|
|
328
|
-
this.handleSharingGroupAddMember(req, res, p);
|
|
329
|
-
else if (p.match(/^\/api\/sharing\/groups\/[^/]+\/members$/) && req.method === "DELETE")
|
|
330
|
-
this.handleSharingGroupRemoveMember(req, res, p);
|
|
331
348
|
else if (p === "/api/sharing/users" && req.method === "GET")
|
|
332
349
|
this.serveSharingUsers(res);
|
|
350
|
+
else if (p === "/api/sharing/notifications" && req.method === "GET")
|
|
351
|
+
this.serveSharingNotifications(res, url);
|
|
352
|
+
else if (p === "/api/sharing/notifications/read" && req.method === "POST")
|
|
353
|
+
this.handleSharingNotificationsRead(req, res);
|
|
354
|
+
else if (p === "/api/sharing/notifications/clear" && req.method === "POST")
|
|
355
|
+
this.handleSharingNotificationsClear(req, res);
|
|
356
|
+
else if (p === "/api/notifications/stream" && req.method === "GET")
|
|
357
|
+
this.handleNotifSSE(req, res);
|
|
333
358
|
else if (p === "/api/admin/shared-tasks" && req.method === "GET")
|
|
334
359
|
this.serveAdminSharedTasks(res);
|
|
360
|
+
else if (p.match(/^\/api\/admin\/shared-tasks\/[^/]+\/detail$/) && req.method === "GET")
|
|
361
|
+
this.serveHubTaskDetail(res, p);
|
|
335
362
|
else if (p.match(/^\/api\/admin\/shared-tasks\/[^/]+$/) && req.method === "DELETE")
|
|
336
363
|
this.handleAdminDeleteTask(res, p);
|
|
337
364
|
else if (p === "/api/admin/shared-skills" && req.method === "GET")
|
|
338
365
|
this.serveAdminSharedSkills(res);
|
|
366
|
+
else if (p.match(/^\/api\/admin\/shared-skills\/[^/]+\/detail$/) && req.method === "GET")
|
|
367
|
+
this.serveHubSkillDetail(res, p);
|
|
339
368
|
else if (p.match(/^\/api\/admin\/shared-skills\/[^/]+$/) && req.method === "DELETE")
|
|
340
369
|
this.handleAdminDeleteSkill(res, p);
|
|
341
370
|
else if (p === "/api/admin/shared-memories" && req.method === "GET")
|
|
@@ -513,7 +542,11 @@ class ViewerServer {
|
|
|
513
542
|
conditions.push("role = ?");
|
|
514
543
|
params.push(role);
|
|
515
544
|
}
|
|
516
|
-
if (owner) {
|
|
545
|
+
if (owner && owner.startsWith("agent:")) {
|
|
546
|
+
conditions.push("(owner = ? OR owner = 'public')");
|
|
547
|
+
params.push(owner);
|
|
548
|
+
}
|
|
549
|
+
else if (owner) {
|
|
517
550
|
conditions.push("owner = ?");
|
|
518
551
|
params.push(owner);
|
|
519
552
|
}
|
|
@@ -527,16 +560,20 @@ class ViewerServer {
|
|
|
527
560
|
}
|
|
528
561
|
const where = conditions.length > 0 ? " WHERE " + conditions.join(" AND ") : "";
|
|
529
562
|
const totalRow = db.prepare("SELECT COUNT(*) as count FROM chunks" + where).get(...params);
|
|
530
|
-
const rawMemories = db.prepare("SELECT * FROM chunks" + where + ` ORDER BY created_at ${sortBy} LIMIT ? OFFSET ?`).all(...params, limit, offset);
|
|
563
|
+
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);
|
|
531
564
|
const findMergeSources = db.prepare("SELECT id, summary, role FROM chunks WHERE dedup_target = ? AND (dedup_status = 'merged' OR dedup_status = 'duplicate')");
|
|
532
565
|
const chunkIds = rawMemories.map((m) => m.id);
|
|
533
566
|
const sharingMap = new Map();
|
|
567
|
+
const localShareMap = new Map();
|
|
534
568
|
if (chunkIds.length > 0) {
|
|
535
569
|
try {
|
|
536
570
|
const placeholders = chunkIds.map(() => "?").join(",");
|
|
537
571
|
const sharedRows = db.prepare(`SELECT source_chunk_id, visibility, group_id FROM hub_memories WHERE source_chunk_id IN (${placeholders})`).all(...chunkIds);
|
|
538
572
|
for (const r of sharedRows)
|
|
539
573
|
sharingMap.set(r.source_chunk_id, r);
|
|
574
|
+
const localRows = db.prepare(`SELECT chunk_id, original_owner, shared_at FROM local_shared_memories WHERE chunk_id IN (${placeholders})`).all(...chunkIds);
|
|
575
|
+
for (const r of localRows)
|
|
576
|
+
localShareMap.set(r.chunk_id, r);
|
|
540
577
|
}
|
|
541
578
|
catch {
|
|
542
579
|
}
|
|
@@ -548,8 +585,12 @@ class ViewerServer {
|
|
|
548
585
|
out.merge_sources = sources;
|
|
549
586
|
}
|
|
550
587
|
const shared = sharingMap.get(m.id);
|
|
588
|
+
const localShared = localShareMap.get(m.id);
|
|
551
589
|
out.sharingVisibility = shared?.visibility ?? null;
|
|
552
590
|
out.sharingGroupId = shared?.group_id ?? null;
|
|
591
|
+
out.localSharing = out.owner === "public";
|
|
592
|
+
out.localSharingManaged = !!localShared;
|
|
593
|
+
out.localOriginalOwner = localShared?.original_owner ?? null;
|
|
553
594
|
return out;
|
|
554
595
|
});
|
|
555
596
|
this.store.recordViewerEvent("list");
|
|
@@ -564,19 +605,35 @@ class ViewerServer {
|
|
|
564
605
|
this.jsonResponse(res, data);
|
|
565
606
|
}
|
|
566
607
|
serveToolMetrics(res, url) {
|
|
567
|
-
const
|
|
608
|
+
const fromParam = url.searchParams.get("from");
|
|
609
|
+
const toParam = url.searchParams.get("to");
|
|
610
|
+
if (fromParam) {
|
|
611
|
+
const fromMs = new Date(fromParam).getTime();
|
|
612
|
+
const toMs = toParam ? new Date(toParam).getTime() : Date.now();
|
|
613
|
+
if (isNaN(fromMs) || isNaN(toMs)) {
|
|
614
|
+
this.jsonResponse(res, { error: "Invalid date" }, 400);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
const diffMin = Math.max(10, Math.min(43200, Math.round((toMs - fromMs) / 60000)));
|
|
618
|
+
const data = this.store.getToolMetrics(diffMin, fromMs, toMs);
|
|
619
|
+
this.jsonResponse(res, data);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
const minutes = Math.min(43200, Math.max(10, Number(url.searchParams.get("minutes")) || 60));
|
|
568
623
|
const data = this.store.getToolMetrics(minutes);
|
|
569
624
|
this.jsonResponse(res, data);
|
|
570
625
|
}
|
|
571
626
|
serveTasks(res, url) {
|
|
572
627
|
this.store.recordViewerEvent("tasks_list");
|
|
573
628
|
const status = url.searchParams.get("status") ?? undefined;
|
|
629
|
+
const owner = url.searchParams.get("owner") ?? undefined;
|
|
574
630
|
const limit = Math.min(100, Math.max(1, Number(url.searchParams.get("limit")) || 50));
|
|
575
631
|
const offset = Math.max(0, Number(url.searchParams.get("offset")) || 0);
|
|
576
|
-
const { tasks, total } = this.store.listTasks({ status, limit, offset });
|
|
632
|
+
const { tasks, total } = this.store.listTasks({ status, limit, offset, owner });
|
|
577
633
|
const db = this.store.db;
|
|
578
634
|
const items = tasks.map((t) => {
|
|
579
|
-
const meta = db.prepare("SELECT skill_status FROM tasks WHERE id = ?").get(t.id);
|
|
635
|
+
const meta = db.prepare("SELECT skill_status, owner FROM tasks WHERE id = ?").get(t.id);
|
|
636
|
+
const sharedTask = db.prepare("SELECT visibility FROM hub_tasks WHERE source_task_id = ? ORDER BY updated_at DESC LIMIT 1").get(t.id);
|
|
580
637
|
return {
|
|
581
638
|
id: t.id,
|
|
582
639
|
sessionKey: t.sessionKey,
|
|
@@ -587,6 +644,8 @@ class ViewerServer {
|
|
|
587
644
|
endedAt: t.endedAt,
|
|
588
645
|
chunkCount: this.store.countChunksByTask(t.id),
|
|
589
646
|
skillStatus: meta?.skill_status ?? null,
|
|
647
|
+
owner: meta?.owner ?? "agent:main",
|
|
648
|
+
sharingVisibility: sharedTask?.visibility ?? null,
|
|
590
649
|
};
|
|
591
650
|
});
|
|
592
651
|
this.jsonResponse(res, { tasks: items, total, limit, offset });
|
|
@@ -622,6 +681,7 @@ class ViewerServer {
|
|
|
622
681
|
title: task.title,
|
|
623
682
|
summary: task.summary,
|
|
624
683
|
status: task.status,
|
|
684
|
+
owner: task.owner ?? "agent:main",
|
|
625
685
|
startedAt: task.startedAt,
|
|
626
686
|
endedAt: task.endedAt,
|
|
627
687
|
chunks: chunkItems,
|
|
@@ -630,6 +690,7 @@ class ViewerServer {
|
|
|
630
690
|
skillLinks,
|
|
631
691
|
sharingVisibility: sharedTask?.visibility ?? null,
|
|
632
692
|
sharingGroupId: sharedTask?.group_id ?? null,
|
|
693
|
+
hubTaskId: sharedTask ? true : false,
|
|
633
694
|
});
|
|
634
695
|
}
|
|
635
696
|
serveStats(res, url) {
|
|
@@ -663,12 +724,21 @@ class ViewerServer {
|
|
|
663
724
|
embCount = db.prepare("SELECT COUNT(*) as count FROM embeddings").get().count;
|
|
664
725
|
}
|
|
665
726
|
catch { /* table may not exist */ }
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
727
|
+
let sessionQuery;
|
|
728
|
+
let sessionParams;
|
|
729
|
+
if (ownerFilter && ownerFilter.startsWith("agent:")) {
|
|
730
|
+
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";
|
|
731
|
+
sessionParams = [ownerFilter];
|
|
732
|
+
}
|
|
733
|
+
else if (ownerFilter) {
|
|
734
|
+
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";
|
|
735
|
+
sessionParams = [ownerFilter];
|
|
736
|
+
}
|
|
737
|
+
else {
|
|
738
|
+
sessionQuery = "SELECT session_key, COUNT(*) as count, MIN(created_at) as earliest, MAX(created_at) as latest FROM chunks GROUP BY session_key ORDER BY latest DESC";
|
|
739
|
+
sessionParams = [];
|
|
740
|
+
}
|
|
741
|
+
const sessionList = db.prepare(sessionQuery).all(...sessionParams);
|
|
672
742
|
let skillCount = 0;
|
|
673
743
|
try {
|
|
674
744
|
skillCount = db.prepare("SELECT COUNT(*) as count FROM skills").get().count;
|
|
@@ -682,10 +752,17 @@ class ViewerServer {
|
|
|
682
752
|
catch { /* column may not exist yet */ }
|
|
683
753
|
let owners = [];
|
|
684
754
|
try {
|
|
685
|
-
const ownerRows = db.prepare("SELECT DISTINCT owner FROM chunks WHERE owner IS NOT NULL ORDER BY owner").all();
|
|
755
|
+
const ownerRows = db.prepare("SELECT DISTINCT owner FROM chunks WHERE owner IS NOT NULL AND owner LIKE 'agent:%' ORDER BY owner").all();
|
|
686
756
|
owners = ownerRows.map((o) => o.owner);
|
|
687
757
|
}
|
|
688
758
|
catch { /* column may not exist yet */ }
|
|
759
|
+
let currentAgentOwner = "agent:main";
|
|
760
|
+
try {
|
|
761
|
+
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();
|
|
762
|
+
if (latest?.owner)
|
|
763
|
+
currentAgentOwner = latest.owner;
|
|
764
|
+
}
|
|
765
|
+
catch { /* best-effort */ }
|
|
689
766
|
this.jsonResponse(res, {
|
|
690
767
|
totalMemories: total.count, totalSessions: sessions.count, totalEmbeddings: embCount,
|
|
691
768
|
totalSkills: skillCount,
|
|
@@ -694,6 +771,7 @@ class ViewerServer {
|
|
|
694
771
|
timeRange: { earliest: timeRange.earliest, latest: timeRange.latest },
|
|
695
772
|
sessions: sessionList,
|
|
696
773
|
owners,
|
|
774
|
+
currentAgentOwner,
|
|
697
775
|
});
|
|
698
776
|
}
|
|
699
777
|
catch (e) {
|
|
@@ -817,7 +895,12 @@ class ViewerServer {
|
|
|
817
895
|
if (visibility) {
|
|
818
896
|
skills = skills.filter(s => s.visibility === visibility);
|
|
819
897
|
}
|
|
820
|
-
|
|
898
|
+
const db = this.store.db;
|
|
899
|
+
const enriched = skills.map(s => {
|
|
900
|
+
const hub = db.prepare("SELECT visibility FROM hub_skills WHERE source_skill_id = ? ORDER BY updated_at DESC LIMIT 1").get(s.id);
|
|
901
|
+
return { ...s, sharingVisibility: hub?.visibility ?? null };
|
|
902
|
+
});
|
|
903
|
+
this.jsonResponse(res, { skills: enriched });
|
|
821
904
|
}
|
|
822
905
|
serveSkillDetail(res, urlPath) {
|
|
823
906
|
const skillId = urlPath.replace("/api/skill/", "");
|
|
@@ -1077,7 +1160,7 @@ class ViewerServer {
|
|
|
1077
1160
|
}
|
|
1078
1161
|
});
|
|
1079
1162
|
}
|
|
1080
|
-
handleSkillDelete(res, urlPath) {
|
|
1163
|
+
async handleSkillDelete(res, urlPath) {
|
|
1081
1164
|
const skillId = urlPath.replace("/api/skill/", "");
|
|
1082
1165
|
const skill = this.store.getSkill(skillId);
|
|
1083
1166
|
if (!skill) {
|
|
@@ -1085,7 +1168,18 @@ class ViewerServer {
|
|
|
1085
1168
|
res.end(JSON.stringify({ error: "Skill not found" }));
|
|
1086
1169
|
return;
|
|
1087
1170
|
}
|
|
1088
|
-
|
|
1171
|
+
try {
|
|
1172
|
+
const hub = this.resolveHubConnection();
|
|
1173
|
+
if (hub) {
|
|
1174
|
+
await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/skills/unpublish", {
|
|
1175
|
+
method: "POST",
|
|
1176
|
+
body: JSON.stringify({ sourceSkillId: skillId }),
|
|
1177
|
+
}).catch(() => { });
|
|
1178
|
+
}
|
|
1179
|
+
const db = this.store.db;
|
|
1180
|
+
db.prepare("DELETE FROM hub_skills WHERE source_skill_id = ?").run(skillId);
|
|
1181
|
+
}
|
|
1182
|
+
catch (_) { }
|
|
1089
1183
|
try {
|
|
1090
1184
|
if (skill.dirPath && node_fs_1.default.existsSync(skill.dirPath)) {
|
|
1091
1185
|
node_fs_1.default.rmSync(skill.dirPath, { recursive: true, force: true });
|
|
@@ -1135,7 +1229,15 @@ class ViewerServer {
|
|
|
1135
1229
|
const cleaned = chunk.role === "user" && chunk.content
|
|
1136
1230
|
? { ...chunk, content: (0, capture_1.stripInboundMetadata)(chunk.content) }
|
|
1137
1231
|
: chunk;
|
|
1138
|
-
this.
|
|
1232
|
+
const localShared = this.store.getLocalSharedMemory(chunkId);
|
|
1233
|
+
this.jsonResponse(res, {
|
|
1234
|
+
memory: {
|
|
1235
|
+
...cleaned,
|
|
1236
|
+
localSharing: cleaned.owner === "public",
|
|
1237
|
+
localSharingManaged: !!localShared,
|
|
1238
|
+
localOriginalOwner: localShared?.originalOwner ?? null,
|
|
1239
|
+
},
|
|
1240
|
+
});
|
|
1139
1241
|
}
|
|
1140
1242
|
handleUpdate(req, res, urlPath) {
|
|
1141
1243
|
const chunkId = urlPath.replace("/api/memory/", "");
|
|
@@ -1170,6 +1272,360 @@ class ViewerServer {
|
|
|
1170
1272
|
res.end(JSON.stringify({ error: "Not found" }));
|
|
1171
1273
|
}
|
|
1172
1274
|
}
|
|
1275
|
+
handleMemoryLocalShare(req, res) {
|
|
1276
|
+
this.readBody(req, (body) => {
|
|
1277
|
+
try {
|
|
1278
|
+
const parsed = JSON.parse(body || "{}");
|
|
1279
|
+
const chunkId = String(parsed.chunkId || "");
|
|
1280
|
+
if (!chunkId)
|
|
1281
|
+
return this.jsonResponse(res, { ok: false, error: "missing_chunk_id" }, 400);
|
|
1282
|
+
const result = this.store.markMemorySharedLocally(chunkId);
|
|
1283
|
+
if (!result.ok) {
|
|
1284
|
+
return this.jsonResponse(res, { ok: false, error: result.reason ?? "share_failed" }, result.reason === "not_found" ? 404 : 400);
|
|
1285
|
+
}
|
|
1286
|
+
this.jsonResponse(res, {
|
|
1287
|
+
ok: true,
|
|
1288
|
+
chunkId,
|
|
1289
|
+
owner: result.owner,
|
|
1290
|
+
localSharing: true,
|
|
1291
|
+
localSharingManaged: true,
|
|
1292
|
+
localOriginalOwner: result.originalOwner ?? null,
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
catch (err) {
|
|
1296
|
+
this.jsonResponse(res, { ok: false, error: String(err) }, 400);
|
|
1297
|
+
}
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
handleMemoryLocalUnshare(req, res) {
|
|
1301
|
+
this.readBody(req, (body) => {
|
|
1302
|
+
try {
|
|
1303
|
+
const parsed = JSON.parse(body || "{}");
|
|
1304
|
+
const chunkId = String(parsed.chunkId || "");
|
|
1305
|
+
const privateOwner = typeof parsed.privateOwner === "string" ? parsed.privateOwner : undefined;
|
|
1306
|
+
if (!chunkId)
|
|
1307
|
+
return this.jsonResponse(res, { ok: false, error: "missing_chunk_id" }, 400);
|
|
1308
|
+
const result = this.store.unmarkMemorySharedLocally(chunkId, privateOwner);
|
|
1309
|
+
if (!result.ok) {
|
|
1310
|
+
return this.jsonResponse(res, { ok: false, error: result.reason ?? "unshare_failed" }, result.reason === "not_found" ? 404 : 400);
|
|
1311
|
+
}
|
|
1312
|
+
this.jsonResponse(res, {
|
|
1313
|
+
ok: true,
|
|
1314
|
+
chunkId,
|
|
1315
|
+
owner: result.owner,
|
|
1316
|
+
localSharing: false,
|
|
1317
|
+
localOriginalOwner: result.originalOwner ?? null,
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
catch (err) {
|
|
1321
|
+
this.jsonResponse(res, { ok: false, error: String(err) }, 400);
|
|
1322
|
+
}
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
// ─── Unified scope API ───
|
|
1326
|
+
handleMemoryScope(req, res, urlPath) {
|
|
1327
|
+
const chunkId = urlPath.split("/")[3];
|
|
1328
|
+
this.readBody(req, async (body) => {
|
|
1329
|
+
try {
|
|
1330
|
+
const parsed = JSON.parse(body || "{}");
|
|
1331
|
+
const scope = parsed.scope;
|
|
1332
|
+
if (!["private", "local", "team"].includes(scope)) {
|
|
1333
|
+
return this.jsonResponse(res, { ok: false, error: "scope must be 'private', 'local', or 'team'" }, 400);
|
|
1334
|
+
}
|
|
1335
|
+
const db = this.store.db;
|
|
1336
|
+
const chunk = db.prepare("SELECT * FROM chunks WHERE id = ?").get(chunkId);
|
|
1337
|
+
if (!chunk)
|
|
1338
|
+
return this.jsonResponse(res, { ok: false, error: "not_found" }, 404);
|
|
1339
|
+
if (chunk.dedup_status && chunk.dedup_status !== "active") {
|
|
1340
|
+
return this.jsonResponse(res, { ok: false, error: "inactive_memory", message: "Merged/duplicate memories cannot be shared" }, 400);
|
|
1341
|
+
}
|
|
1342
|
+
const isLocalShared = chunk.owner === "public";
|
|
1343
|
+
const hubMemory = this.getHubMemoryForChunk(chunkId);
|
|
1344
|
+
const isTeamShared = !!hubMemory;
|
|
1345
|
+
const currentScope = isTeamShared ? "team" : isLocalShared ? "local" : "private";
|
|
1346
|
+
if (scope === currentScope) {
|
|
1347
|
+
return this.jsonResponse(res, { ok: true, scope, changed: false });
|
|
1348
|
+
}
|
|
1349
|
+
let hubSynced = false;
|
|
1350
|
+
if (scope === "team") {
|
|
1351
|
+
if (!isTeamShared) {
|
|
1352
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1353
|
+
const refreshedChunk = db.prepare("SELECT * FROM chunks WHERE id = ?").get(chunkId);
|
|
1354
|
+
const response = await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/share", {
|
|
1355
|
+
method: "POST",
|
|
1356
|
+
body: JSON.stringify({ memory: { sourceChunkId: refreshedChunk.id, role: refreshedChunk.role, content: refreshedChunk.content, summary: refreshedChunk.summary, kind: refreshedChunk.kind, groupId: null, visibility: "public" } }),
|
|
1357
|
+
});
|
|
1358
|
+
if (!isLocalShared)
|
|
1359
|
+
this.store.markMemorySharedLocally(chunkId);
|
|
1360
|
+
if (hubClient.userId) {
|
|
1361
|
+
const existing = this.store.getHubMemoryBySource(hubClient.userId, chunkId);
|
|
1362
|
+
this.store.upsertHubMemory({
|
|
1363
|
+
id: response?.memoryId ?? existing?.id ?? node_crypto_1.default.randomUUID(),
|
|
1364
|
+
sourceChunkId: chunkId, sourceUserId: hubClient.userId,
|
|
1365
|
+
role: refreshedChunk.role, content: refreshedChunk.content, summary: refreshedChunk.summary ?? "",
|
|
1366
|
+
kind: refreshedChunk.kind, groupId: null, visibility: "public",
|
|
1367
|
+
createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(),
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
hubSynced = true;
|
|
1371
|
+
}
|
|
1372
|
+
else {
|
|
1373
|
+
if (!isLocalShared)
|
|
1374
|
+
this.store.markMemorySharedLocally(chunkId);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
else if (scope === "local") {
|
|
1378
|
+
if (isTeamShared) {
|
|
1379
|
+
try {
|
|
1380
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1381
|
+
await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/unshare", {
|
|
1382
|
+
method: "POST", body: JSON.stringify({ sourceChunkId: chunkId }),
|
|
1383
|
+
});
|
|
1384
|
+
if (hubClient.userId)
|
|
1385
|
+
this.store.deleteHubMemoryBySource(hubClient.userId, chunkId);
|
|
1386
|
+
hubSynced = true;
|
|
1387
|
+
}
|
|
1388
|
+
catch (err) {
|
|
1389
|
+
this.log.warn(`Failed to unshare memory from team: ${err}`);
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
if (!isLocalShared)
|
|
1393
|
+
this.store.markMemorySharedLocally(chunkId);
|
|
1394
|
+
}
|
|
1395
|
+
else {
|
|
1396
|
+
if (isTeamShared) {
|
|
1397
|
+
try {
|
|
1398
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1399
|
+
await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/unshare", {
|
|
1400
|
+
method: "POST", body: JSON.stringify({ sourceChunkId: chunkId }),
|
|
1401
|
+
});
|
|
1402
|
+
if (hubClient.userId)
|
|
1403
|
+
this.store.deleteHubMemoryBySource(hubClient.userId, chunkId);
|
|
1404
|
+
hubSynced = true;
|
|
1405
|
+
}
|
|
1406
|
+
catch (err) {
|
|
1407
|
+
this.log.warn(`Failed to unshare memory from team: ${err}`);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
if (isLocalShared)
|
|
1411
|
+
this.store.unmarkMemorySharedLocally(chunkId);
|
|
1412
|
+
}
|
|
1413
|
+
this.jsonResponse(res, { ok: true, scope, changed: true, hubSynced });
|
|
1414
|
+
}
|
|
1415
|
+
catch (err) {
|
|
1416
|
+
this.jsonResponse(res, { ok: false, error: String(err) }, 500);
|
|
1417
|
+
}
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
handleTaskScope(req, res, urlPath) {
|
|
1421
|
+
const taskId = urlPath.split("/")[3];
|
|
1422
|
+
this.readBody(req, async (body) => {
|
|
1423
|
+
try {
|
|
1424
|
+
const parsed = JSON.parse(body || "{}");
|
|
1425
|
+
const scope = parsed.scope;
|
|
1426
|
+
if (!["private", "local", "team"].includes(scope)) {
|
|
1427
|
+
return this.jsonResponse(res, { ok: false, error: "scope must be 'private', 'local', or 'team'" }, 400);
|
|
1428
|
+
}
|
|
1429
|
+
const task = this.store.getTask(taskId);
|
|
1430
|
+
if (!task)
|
|
1431
|
+
return this.jsonResponse(res, { ok: false, error: "task_not_found" }, 404);
|
|
1432
|
+
if (scope !== "private" && task.status !== "completed") {
|
|
1433
|
+
return this.jsonResponse(res, { ok: false, error: "only_completed_tasks_can_be_shared" }, 400);
|
|
1434
|
+
}
|
|
1435
|
+
const isLocalShared = task.owner === "public";
|
|
1436
|
+
const hubTask = this.getHubTaskForLocal(taskId);
|
|
1437
|
+
const isTeamShared = !!hubTask;
|
|
1438
|
+
const currentScope = isTeamShared ? "team" : isLocalShared ? "local" : "private";
|
|
1439
|
+
if (scope === currentScope) {
|
|
1440
|
+
return this.jsonResponse(res, { ok: true, scope, changed: false });
|
|
1441
|
+
}
|
|
1442
|
+
let hubSynced = false;
|
|
1443
|
+
if (scope === "team") {
|
|
1444
|
+
if (!isTeamShared) {
|
|
1445
|
+
const chunks = this.store.getChunksByTask(taskId);
|
|
1446
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1447
|
+
const refreshedTask = this.store.getTask(taskId);
|
|
1448
|
+
const response = await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/share", {
|
|
1449
|
+
method: "POST",
|
|
1450
|
+
body: JSON.stringify({
|
|
1451
|
+
task: { id: refreshedTask.id, sourceTaskId: refreshedTask.id, title: refreshedTask.title, summary: refreshedTask.summary, groupId: null, visibility: "public", createdAt: refreshedTask.startedAt ?? Date.now(), updatedAt: refreshedTask.updatedAt ?? Date.now() },
|
|
1452
|
+
chunks: chunks.map((c) => ({ id: c.id, hubTaskId: refreshedTask.id, sourceTaskId: refreshedTask.id, sourceChunkId: c.id, role: c.role, content: c.content, summary: c.summary, kind: c.kind, groupId: null, visibility: "public", createdAt: c.createdAt ?? Date.now() })),
|
|
1453
|
+
}),
|
|
1454
|
+
});
|
|
1455
|
+
if (hubClient.userId) {
|
|
1456
|
+
const existing = this.store.getHubTaskBySource(hubClient.userId, taskId);
|
|
1457
|
+
this.store.upsertHubTask({
|
|
1458
|
+
id: response?.taskId ?? existing?.id ?? node_crypto_1.default.randomUUID(),
|
|
1459
|
+
sourceTaskId: taskId, sourceUserId: hubClient.userId, title: refreshedTask.title ?? "",
|
|
1460
|
+
summary: refreshedTask.summary ?? "", groupId: null, visibility: "public",
|
|
1461
|
+
createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(),
|
|
1462
|
+
});
|
|
1463
|
+
}
|
|
1464
|
+
hubSynced = true;
|
|
1465
|
+
}
|
|
1466
|
+
if (!isLocalShared) {
|
|
1467
|
+
const originalOwner = task.owner;
|
|
1468
|
+
const db = this.store.db;
|
|
1469
|
+
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());
|
|
1470
|
+
db.prepare("UPDATE tasks SET owner = 'public' WHERE id = ?").run(taskId);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
if (scope === "local") {
|
|
1474
|
+
if (!isLocalShared) {
|
|
1475
|
+
const originalOwner = task.owner;
|
|
1476
|
+
const db = this.store.db;
|
|
1477
|
+
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());
|
|
1478
|
+
db.prepare("UPDATE tasks SET owner = 'public' WHERE id = ?").run(taskId);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
if (scope === "local" && isTeamShared) {
|
|
1482
|
+
try {
|
|
1483
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1484
|
+
await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/unshare", {
|
|
1485
|
+
method: "POST", body: JSON.stringify({ sourceTaskId: taskId }),
|
|
1486
|
+
});
|
|
1487
|
+
if (hubClient.userId)
|
|
1488
|
+
this.store.deleteHubTaskBySource(hubClient.userId, taskId);
|
|
1489
|
+
hubSynced = true;
|
|
1490
|
+
}
|
|
1491
|
+
catch (err) {
|
|
1492
|
+
this.log.warn(`Failed to unshare task from team: ${err}`);
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
if (scope === "private") {
|
|
1496
|
+
if (isTeamShared) {
|
|
1497
|
+
try {
|
|
1498
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1499
|
+
await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/unshare", {
|
|
1500
|
+
method: "POST", body: JSON.stringify({ sourceTaskId: taskId }),
|
|
1501
|
+
});
|
|
1502
|
+
if (hubClient.userId)
|
|
1503
|
+
this.store.deleteHubTaskBySource(hubClient.userId, taskId);
|
|
1504
|
+
hubSynced = true;
|
|
1505
|
+
}
|
|
1506
|
+
catch (err) {
|
|
1507
|
+
this.log.warn(`Failed to unshare task from team: ${err}`);
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
if (isLocalShared) {
|
|
1511
|
+
const db = this.store.db;
|
|
1512
|
+
const shared = db.prepare("SELECT original_owner FROM local_shared_tasks WHERE task_id = ?").get(taskId);
|
|
1513
|
+
const restoreOwner = shared?.original_owner ?? task.owner;
|
|
1514
|
+
if (restoreOwner && restoreOwner !== "public") {
|
|
1515
|
+
db.prepare("UPDATE tasks SET owner = ? WHERE id = ?").run(restoreOwner, taskId);
|
|
1516
|
+
}
|
|
1517
|
+
db.prepare("DELETE FROM local_shared_tasks WHERE task_id = ?").run(taskId);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
this.jsonResponse(res, { ok: true, scope, changed: true, hubSynced });
|
|
1521
|
+
}
|
|
1522
|
+
catch (err) {
|
|
1523
|
+
this.jsonResponse(res, { ok: false, error: String(err) }, 500);
|
|
1524
|
+
}
|
|
1525
|
+
});
|
|
1526
|
+
}
|
|
1527
|
+
handleSkillScope(req, res, urlPath) {
|
|
1528
|
+
const skillId = urlPath.split("/")[3];
|
|
1529
|
+
this.readBody(req, async (body) => {
|
|
1530
|
+
try {
|
|
1531
|
+
const parsed = JSON.parse(body || "{}");
|
|
1532
|
+
const scope = parsed.scope;
|
|
1533
|
+
if (!["private", "local", "team"].includes(scope)) {
|
|
1534
|
+
return this.jsonResponse(res, { ok: false, error: "scope must be 'private', 'local', or 'team'" }, 400);
|
|
1535
|
+
}
|
|
1536
|
+
const skill = this.store.getSkill(skillId);
|
|
1537
|
+
if (!skill)
|
|
1538
|
+
return this.jsonResponse(res, { ok: false, error: "skill_not_found" }, 404);
|
|
1539
|
+
if (scope !== "private" && skill.status !== "active") {
|
|
1540
|
+
return this.jsonResponse(res, { ok: false, error: "only_active_skills_can_be_shared" }, 400);
|
|
1541
|
+
}
|
|
1542
|
+
const isLocalShared = skill.visibility === "public";
|
|
1543
|
+
const hubSkill = this.getHubSkillForLocal(skillId);
|
|
1544
|
+
const isTeamShared = !!hubSkill;
|
|
1545
|
+
const currentScope = isTeamShared ? "team" : isLocalShared ? "local" : "private";
|
|
1546
|
+
if (scope === currentScope) {
|
|
1547
|
+
return this.jsonResponse(res, { ok: true, scope, changed: false });
|
|
1548
|
+
}
|
|
1549
|
+
let hubSynced = false;
|
|
1550
|
+
if (scope === "team") {
|
|
1551
|
+
if (!isTeamShared) {
|
|
1552
|
+
const bundle = (0, skill_sync_1.buildSkillBundleForHub)(this.store, skillId);
|
|
1553
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1554
|
+
const response = await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/publish", {
|
|
1555
|
+
method: "POST",
|
|
1556
|
+
body: JSON.stringify({ visibility: "public", groupId: null, metadata: bundle.metadata, bundle: bundle.bundle }),
|
|
1557
|
+
});
|
|
1558
|
+
if (hubClient.userId) {
|
|
1559
|
+
const existing = this.store.getHubSkillBySource(hubClient.userId, skillId);
|
|
1560
|
+
this.store.upsertHubSkill({
|
|
1561
|
+
id: response?.skillId ?? existing?.id ?? node_crypto_1.default.randomUUID(),
|
|
1562
|
+
sourceSkillId: skillId, sourceUserId: hubClient.userId,
|
|
1563
|
+
name: skill.name, description: skill.description, version: skill.version,
|
|
1564
|
+
groupId: null, visibility: "public",
|
|
1565
|
+
bundle: JSON.stringify(bundle.bundle), qualityScore: skill.qualityScore,
|
|
1566
|
+
createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(),
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
1569
|
+
hubSynced = true;
|
|
1570
|
+
}
|
|
1571
|
+
if (!isLocalShared)
|
|
1572
|
+
this.store.setSkillVisibility(skillId, "public");
|
|
1573
|
+
}
|
|
1574
|
+
if (scope === "local") {
|
|
1575
|
+
if (!isLocalShared)
|
|
1576
|
+
this.store.setSkillVisibility(skillId, "public");
|
|
1577
|
+
}
|
|
1578
|
+
if (scope === "local" && isTeamShared) {
|
|
1579
|
+
try {
|
|
1580
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1581
|
+
await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/unpublish", {
|
|
1582
|
+
method: "POST", body: JSON.stringify({ sourceSkillId: skillId }),
|
|
1583
|
+
});
|
|
1584
|
+
if (hubClient.userId)
|
|
1585
|
+
this.store.deleteHubSkillBySource(hubClient.userId, skillId);
|
|
1586
|
+
hubSynced = true;
|
|
1587
|
+
}
|
|
1588
|
+
catch (err) {
|
|
1589
|
+
this.log.warn(`Failed to unpublish skill from team: ${err}`);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
if (scope === "private") {
|
|
1593
|
+
if (isTeamShared) {
|
|
1594
|
+
try {
|
|
1595
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1596
|
+
await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/unpublish", {
|
|
1597
|
+
method: "POST", body: JSON.stringify({ sourceSkillId: skillId }),
|
|
1598
|
+
});
|
|
1599
|
+
if (hubClient.userId)
|
|
1600
|
+
this.store.deleteHubSkillBySource(hubClient.userId, skillId);
|
|
1601
|
+
hubSynced = true;
|
|
1602
|
+
}
|
|
1603
|
+
catch (err) {
|
|
1604
|
+
this.log.warn(`Failed to unpublish skill from team: ${err}`);
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
if (isLocalShared)
|
|
1608
|
+
this.store.setSkillVisibility(skillId, "private");
|
|
1609
|
+
}
|
|
1610
|
+
this.jsonResponse(res, { ok: true, scope, changed: true, hubSynced });
|
|
1611
|
+
}
|
|
1612
|
+
catch (err) {
|
|
1613
|
+
this.jsonResponse(res, { ok: false, error: String(err) }, 500);
|
|
1614
|
+
}
|
|
1615
|
+
});
|
|
1616
|
+
}
|
|
1617
|
+
getHubMemoryForChunk(chunkId) {
|
|
1618
|
+
const db = this.store.db;
|
|
1619
|
+
return db.prepare("SELECT * FROM hub_memories WHERE source_chunk_id = ? LIMIT 1").get(chunkId);
|
|
1620
|
+
}
|
|
1621
|
+
getHubTaskForLocal(taskId) {
|
|
1622
|
+
const db = this.store.db;
|
|
1623
|
+
return db.prepare("SELECT * FROM hub_tasks WHERE source_task_id = ? LIMIT 1").get(taskId);
|
|
1624
|
+
}
|
|
1625
|
+
getHubSkillForLocal(skillId) {
|
|
1626
|
+
const db = this.store.db;
|
|
1627
|
+
return db.prepare("SELECT * FROM hub_skills WHERE source_skill_id = ? LIMIT 1").get(skillId);
|
|
1628
|
+
}
|
|
1173
1629
|
handleDeleteSession(res, url) {
|
|
1174
1630
|
const key = url.searchParams.get("key");
|
|
1175
1631
|
if (!key) {
|
|
@@ -1207,7 +1663,8 @@ class ViewerServer {
|
|
|
1207
1663
|
// ─── Config API ───
|
|
1208
1664
|
getOpenClawConfigPath() {
|
|
1209
1665
|
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
1210
|
-
|
|
1666
|
+
const ocHome = process.env.OPENCLAW_STATE_DIR || node_path_1.default.join(home, ".openclaw");
|
|
1667
|
+
return node_path_1.default.join(ocHome, "openclaw.json");
|
|
1211
1668
|
}
|
|
1212
1669
|
getPluginEntryConfig(raw) {
|
|
1213
1670
|
const entries = raw?.plugins?.entries ?? {};
|
|
@@ -1263,8 +1720,7 @@ class ViewerServer {
|
|
|
1263
1720
|
base.admin.rejectSupported = true;
|
|
1264
1721
|
base.connection.connected = true;
|
|
1265
1722
|
base.connection.hubUrl = resolvedHubUrl ?? undefined;
|
|
1266
|
-
|
|
1267
|
-
let adminUser = { username: "hub-admin", role: "admin", groups: [] };
|
|
1723
|
+
let adminUser = { username: "hub-admin", role: "admin" };
|
|
1268
1724
|
try {
|
|
1269
1725
|
const hub = this.resolveHubConnection();
|
|
1270
1726
|
if (hub) {
|
|
@@ -1274,7 +1730,6 @@ class ViewerServer {
|
|
|
1274
1730
|
id: me.id,
|
|
1275
1731
|
username: me.username ?? "hub-admin",
|
|
1276
1732
|
role: me.role ?? "admin",
|
|
1277
|
-
groups: Array.isArray(me.groups) ? me.groups : [],
|
|
1278
1733
|
};
|
|
1279
1734
|
}
|
|
1280
1735
|
}
|
|
@@ -1306,6 +1761,9 @@ class ViewerServer {
|
|
|
1306
1761
|
if (status.user?.status === "rejected") {
|
|
1307
1762
|
output.connection.rejected = true;
|
|
1308
1763
|
}
|
|
1764
|
+
if (status.user?.status === "removed") {
|
|
1765
|
+
output.connection.removed = true;
|
|
1766
|
+
}
|
|
1309
1767
|
if (status.connected && status.hubUrl) {
|
|
1310
1768
|
try {
|
|
1311
1769
|
const info = await fetch(`${status.hubUrl}/api/v1/hub/info`).then((r) => (r.ok ? r.json() : null)).catch(() => null);
|
|
@@ -1383,6 +1841,75 @@ class ViewerServer {
|
|
|
1383
1841
|
}
|
|
1384
1842
|
});
|
|
1385
1843
|
}
|
|
1844
|
+
handleSharingChangeRole(req, res) {
|
|
1845
|
+
this.readBody(req, async (body) => {
|
|
1846
|
+
if (!this.ctx)
|
|
1847
|
+
return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
1848
|
+
try {
|
|
1849
|
+
const parsed = JSON.parse(body || "{}");
|
|
1850
|
+
const hub = this.resolveHubConnection();
|
|
1851
|
+
if (!hub)
|
|
1852
|
+
return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1853
|
+
const result = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/change-role", {
|
|
1854
|
+
method: "POST",
|
|
1855
|
+
body: JSON.stringify({ userId: parsed.userId, role: parsed.role }),
|
|
1856
|
+
});
|
|
1857
|
+
this.jsonResponse(res, { ok: true, result });
|
|
1858
|
+
}
|
|
1859
|
+
catch (err) {
|
|
1860
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1861
|
+
}
|
|
1862
|
+
});
|
|
1863
|
+
}
|
|
1864
|
+
handleSharingRemoveUser(req, res) {
|
|
1865
|
+
this.readBody(req, async (body) => {
|
|
1866
|
+
if (!this.ctx)
|
|
1867
|
+
return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
1868
|
+
try {
|
|
1869
|
+
const parsed = JSON.parse(body || "{}");
|
|
1870
|
+
const hub = this.resolveHubConnection();
|
|
1871
|
+
if (!hub)
|
|
1872
|
+
return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1873
|
+
const result = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/remove-user", {
|
|
1874
|
+
method: "POST",
|
|
1875
|
+
body: JSON.stringify({ userId: parsed.userId, cleanResources: parsed.cleanResources === true }),
|
|
1876
|
+
});
|
|
1877
|
+
this.jsonResponse(res, { ok: true, result });
|
|
1878
|
+
}
|
|
1879
|
+
catch (err) {
|
|
1880
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1881
|
+
}
|
|
1882
|
+
});
|
|
1883
|
+
}
|
|
1884
|
+
handleAdminRenameUser(req, res) {
|
|
1885
|
+
this.readBody(req, async (body) => {
|
|
1886
|
+
if (!this.ctx)
|
|
1887
|
+
return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
1888
|
+
try {
|
|
1889
|
+
const parsed = JSON.parse(body || "{}");
|
|
1890
|
+
const hub = this.resolveHubConnection();
|
|
1891
|
+
if (!hub)
|
|
1892
|
+
return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1893
|
+
const result = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/rename-user", {
|
|
1894
|
+
method: "POST",
|
|
1895
|
+
body: JSON.stringify({ userId: parsed.userId, username: parsed.username }),
|
|
1896
|
+
});
|
|
1897
|
+
this.jsonResponse(res, { ok: true, result });
|
|
1898
|
+
}
|
|
1899
|
+
catch (err) {
|
|
1900
|
+
const errStr = String(err);
|
|
1901
|
+
if (errStr.includes("username_taken")) {
|
|
1902
|
+
this.jsonResponse(res, { ok: false, error: "username_taken" });
|
|
1903
|
+
}
|
|
1904
|
+
else if (errStr.includes("invalid_params")) {
|
|
1905
|
+
this.jsonResponse(res, { ok: false, error: "invalid_params" });
|
|
1906
|
+
}
|
|
1907
|
+
else {
|
|
1908
|
+
this.jsonResponse(res, { ok: false, error: errStr });
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
});
|
|
1912
|
+
}
|
|
1386
1913
|
handleRetryJoin(req, res) {
|
|
1387
1914
|
this.readBody(req, async (_body) => {
|
|
1388
1915
|
if (!this.ctx)
|
|
@@ -1398,12 +1925,22 @@ class ViewerServer {
|
|
|
1398
1925
|
}
|
|
1399
1926
|
try {
|
|
1400
1927
|
const hubUrl = (0, hub_1.normalizeHubUrl)(hubAddress);
|
|
1928
|
+
const localIPs = this.getLocalIPs();
|
|
1929
|
+
localIPs.push("127.0.0.1", "localhost", "0.0.0.0");
|
|
1930
|
+
try {
|
|
1931
|
+
const u = new URL(hubUrl);
|
|
1932
|
+
if (localIPs.includes(u.hostname)) {
|
|
1933
|
+
return this.jsonResponse(res, { ok: false, error: "cannot_join_self" });
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
catch { }
|
|
1401
1937
|
const os = await Promise.resolve().then(() => __importStar(require("os")));
|
|
1402
|
-
const
|
|
1938
|
+
const nickname = sharing.client?.nickname;
|
|
1939
|
+
const username = nickname || os.userInfo().username || "user";
|
|
1403
1940
|
const hostname = os.hostname() || "unknown";
|
|
1404
1941
|
const result = await (0, hub_1.hubRequestJson)(hubUrl, "", "/api/v1/hub/join", {
|
|
1405
1942
|
method: "POST",
|
|
1406
|
-
body: JSON.stringify({ teamToken, username, deviceName: hostname }),
|
|
1943
|
+
body: JSON.stringify({ teamToken, username, deviceName: hostname, reapply: true }),
|
|
1407
1944
|
});
|
|
1408
1945
|
this.store.setClientHubConnection({
|
|
1409
1946
|
hubUrl,
|
|
@@ -1425,7 +1962,14 @@ class ViewerServer {
|
|
|
1425
1962
|
return this.jsonResponse(res, { memories: [], error: "sharing_unavailable" });
|
|
1426
1963
|
try {
|
|
1427
1964
|
const limit = Number(url.searchParams.get("limit") || 40);
|
|
1428
|
-
const
|
|
1965
|
+
const hub = this.resolveHubConnection();
|
|
1966
|
+
let data;
|
|
1967
|
+
if (hub) {
|
|
1968
|
+
data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/memories?limit=${limit}`);
|
|
1969
|
+
}
|
|
1970
|
+
else {
|
|
1971
|
+
data = await (0, hub_1.hubListMemories)(this.store, this.ctx, { limit });
|
|
1972
|
+
}
|
|
1429
1973
|
this.jsonResponse(res, { memories: Array.isArray(data?.memories) ? data.memories : [] });
|
|
1430
1974
|
}
|
|
1431
1975
|
catch (err) {
|
|
@@ -1437,7 +1981,14 @@ class ViewerServer {
|
|
|
1437
1981
|
return this.jsonResponse(res, { tasks: [], error: "sharing_unavailable" });
|
|
1438
1982
|
try {
|
|
1439
1983
|
const limit = Number(url.searchParams.get("limit") || 40);
|
|
1440
|
-
const
|
|
1984
|
+
const hub = this.resolveHubConnection();
|
|
1985
|
+
let data;
|
|
1986
|
+
if (hub) {
|
|
1987
|
+
data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/tasks?limit=${limit}`);
|
|
1988
|
+
}
|
|
1989
|
+
else {
|
|
1990
|
+
data = await (0, hub_1.hubListTasks)(this.store, this.ctx, { limit });
|
|
1991
|
+
}
|
|
1441
1992
|
this.jsonResponse(res, { tasks: Array.isArray(data?.tasks) ? data.tasks : [] });
|
|
1442
1993
|
}
|
|
1443
1994
|
catch (err) {
|
|
@@ -1449,7 +2000,14 @@ class ViewerServer {
|
|
|
1449
2000
|
return this.jsonResponse(res, { skills: [], error: "sharing_unavailable" });
|
|
1450
2001
|
try {
|
|
1451
2002
|
const limit = Number(url.searchParams.get("limit") || 40);
|
|
1452
|
-
const
|
|
2003
|
+
const hub = this.resolveHubConnection();
|
|
2004
|
+
let data;
|
|
2005
|
+
if (hub) {
|
|
2006
|
+
data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/skills/list?limit=${limit}`);
|
|
2007
|
+
}
|
|
2008
|
+
else {
|
|
2009
|
+
data = await (0, hub_1.hubListSkills)(this.store, this.ctx, { limit });
|
|
2010
|
+
}
|
|
1453
2011
|
this.jsonResponse(res, { skills: Array.isArray(data?.skills) ? data.skills : [] });
|
|
1454
2012
|
}
|
|
1455
2013
|
catch (err) {
|
|
@@ -1466,13 +2024,22 @@ class ViewerServer {
|
|
|
1466
2024
|
const query = String(parsed.query || "");
|
|
1467
2025
|
const role = typeof parsed.role === "string" ? parsed.role : undefined;
|
|
1468
2026
|
const maxResults = typeof parsed.maxResults === "number" ? parsed.maxResults : 10;
|
|
1469
|
-
const scope = parsed.scope === "group" || parsed.scope === "all" ? parsed.scope : "local";
|
|
2027
|
+
const scope = parsed.scope === "group" || parsed.scope === "all" || parsed.scope === "hub" ? (parsed.scope === "hub" ? "all" : parsed.scope) : "local";
|
|
1470
2028
|
const local = this.searchLocalViewerMemories(query, { role, maxResults });
|
|
1471
2029
|
if (scope === "local") {
|
|
1472
2030
|
return this.jsonResponse(res, { local: { hits: local.hits, meta: local.meta }, hub: emptyHub });
|
|
1473
2031
|
}
|
|
1474
2032
|
try {
|
|
1475
|
-
const
|
|
2033
|
+
const conn = this.resolveHubConnection();
|
|
2034
|
+
let hub;
|
|
2035
|
+
if (conn) {
|
|
2036
|
+
hub = await (0, hub_1.hubRequestJson)(conn.hubUrl, conn.userToken, "/api/v1/hub/search", {
|
|
2037
|
+
method: "POST", body: JSON.stringify({ query, maxResults, scope }),
|
|
2038
|
+
});
|
|
2039
|
+
}
|
|
2040
|
+
else {
|
|
2041
|
+
hub = await (0, hub_1.hubSearchMemories)(this.store, this.ctx, { query, maxResults, scope });
|
|
2042
|
+
}
|
|
1476
2043
|
this.jsonResponse(res, { local: { hits: local.hits, meta: local.meta }, hub });
|
|
1477
2044
|
}
|
|
1478
2045
|
catch (err) {
|
|
@@ -1860,123 +2427,6 @@ class ViewerServer {
|
|
|
1860
2427
|
}
|
|
1861
2428
|
return (0, hub_1.resolveHubClient)(this.store, this.ctx);
|
|
1862
2429
|
}
|
|
1863
|
-
extractGroupId(path) {
|
|
1864
|
-
const m = path.match(/\/api\/sharing\/groups\/([^/]+)/);
|
|
1865
|
-
return m ? decodeURIComponent(m[1]) : "";
|
|
1866
|
-
}
|
|
1867
|
-
async serveSharingGroups(res) {
|
|
1868
|
-
const hub = this.resolveHubConnection();
|
|
1869
|
-
if (!hub)
|
|
1870
|
-
return this.jsonResponse(res, { groups: [], error: "not_configured" });
|
|
1871
|
-
try {
|
|
1872
|
-
const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/groups", { method: "GET" });
|
|
1873
|
-
this.jsonResponse(res, { groups: Array.isArray(data?.groups) ? data.groups : [] });
|
|
1874
|
-
}
|
|
1875
|
-
catch (err) {
|
|
1876
|
-
this.jsonResponse(res, { groups: [], error: String(err) });
|
|
1877
|
-
}
|
|
1878
|
-
}
|
|
1879
|
-
handleSharingGroupCreate(req, res) {
|
|
1880
|
-
this.readBody(req, async (body) => {
|
|
1881
|
-
const hub = this.resolveHubConnection();
|
|
1882
|
-
if (!hub)
|
|
1883
|
-
return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1884
|
-
try {
|
|
1885
|
-
const parsed = JSON.parse(body || "{}");
|
|
1886
|
-
const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/groups", {
|
|
1887
|
-
method: "POST",
|
|
1888
|
-
body: JSON.stringify({ name: parsed.name, description: parsed.description }),
|
|
1889
|
-
});
|
|
1890
|
-
this.jsonResponse(res, { ok: true, ...data });
|
|
1891
|
-
}
|
|
1892
|
-
catch (err) {
|
|
1893
|
-
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1894
|
-
}
|
|
1895
|
-
});
|
|
1896
|
-
}
|
|
1897
|
-
handleSharingGroupUpdate(req, res, p) {
|
|
1898
|
-
this.readBody(req, async (body) => {
|
|
1899
|
-
const hub = this.resolveHubConnection();
|
|
1900
|
-
if (!hub)
|
|
1901
|
-
return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1902
|
-
const groupId = this.extractGroupId(p);
|
|
1903
|
-
try {
|
|
1904
|
-
const parsed = JSON.parse(body || "{}");
|
|
1905
|
-
await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}`, {
|
|
1906
|
-
method: "PUT",
|
|
1907
|
-
body: JSON.stringify({ name: parsed.name, description: parsed.description }),
|
|
1908
|
-
});
|
|
1909
|
-
this.jsonResponse(res, { ok: true });
|
|
1910
|
-
}
|
|
1911
|
-
catch (err) {
|
|
1912
|
-
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1913
|
-
}
|
|
1914
|
-
});
|
|
1915
|
-
}
|
|
1916
|
-
async handleSharingGroupDelete(res, p) {
|
|
1917
|
-
const hub = this.resolveHubConnection();
|
|
1918
|
-
if (!hub)
|
|
1919
|
-
return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1920
|
-
const groupId = this.extractGroupId(p);
|
|
1921
|
-
try {
|
|
1922
|
-
await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}`, { method: "DELETE" });
|
|
1923
|
-
this.jsonResponse(res, { ok: true });
|
|
1924
|
-
}
|
|
1925
|
-
catch (err) {
|
|
1926
|
-
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1927
|
-
}
|
|
1928
|
-
}
|
|
1929
|
-
async serveSharingGroupMembers(res, p) {
|
|
1930
|
-
const hub = this.resolveHubConnection();
|
|
1931
|
-
if (!hub)
|
|
1932
|
-
return this.jsonResponse(res, { members: [], error: "not_configured" });
|
|
1933
|
-
const groupId = this.extractGroupId(p);
|
|
1934
|
-
try {
|
|
1935
|
-
const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}`, { method: "GET" });
|
|
1936
|
-
this.jsonResponse(res, { members: Array.isArray(data?.members) ? data.members : [] });
|
|
1937
|
-
}
|
|
1938
|
-
catch (err) {
|
|
1939
|
-
this.jsonResponse(res, { members: [], error: String(err) });
|
|
1940
|
-
}
|
|
1941
|
-
}
|
|
1942
|
-
handleSharingGroupAddMember(req, res, p) {
|
|
1943
|
-
this.readBody(req, async (body) => {
|
|
1944
|
-
const hub = this.resolveHubConnection();
|
|
1945
|
-
if (!hub)
|
|
1946
|
-
return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1947
|
-
const groupId = this.extractGroupId(p);
|
|
1948
|
-
try {
|
|
1949
|
-
const parsed = JSON.parse(body || "{}");
|
|
1950
|
-
await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}/members`, {
|
|
1951
|
-
method: "POST",
|
|
1952
|
-
body: JSON.stringify({ userId: parsed.userId }),
|
|
1953
|
-
});
|
|
1954
|
-
this.jsonResponse(res, { ok: true });
|
|
1955
|
-
}
|
|
1956
|
-
catch (err) {
|
|
1957
|
-
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1958
|
-
}
|
|
1959
|
-
});
|
|
1960
|
-
}
|
|
1961
|
-
handleSharingGroupRemoveMember(req, res, p) {
|
|
1962
|
-
this.readBody(req, async (body) => {
|
|
1963
|
-
const hub = this.resolveHubConnection();
|
|
1964
|
-
if (!hub)
|
|
1965
|
-
return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1966
|
-
const groupId = this.extractGroupId(p);
|
|
1967
|
-
try {
|
|
1968
|
-
const parsed = JSON.parse(body || "{}");
|
|
1969
|
-
await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}/members`, {
|
|
1970
|
-
method: "DELETE",
|
|
1971
|
-
body: JSON.stringify({ userId: parsed.userId }),
|
|
1972
|
-
});
|
|
1973
|
-
this.jsonResponse(res, { ok: true });
|
|
1974
|
-
}
|
|
1975
|
-
catch (err) {
|
|
1976
|
-
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1977
|
-
}
|
|
1978
|
-
});
|
|
1979
|
-
}
|
|
1980
2430
|
async serveSharingUsers(res) {
|
|
1981
2431
|
const hub = this.resolveHubConnection();
|
|
1982
2432
|
if (!hub)
|
|
@@ -1996,7 +2446,17 @@ class ViewerServer {
|
|
|
1996
2446
|
return this.jsonResponse(res, { tasks: [], error: "not_configured" });
|
|
1997
2447
|
try {
|
|
1998
2448
|
const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/shared-tasks", { method: "GET" });
|
|
1999
|
-
|
|
2449
|
+
const tasks = Array.isArray(data?.tasks) ? data.tasks : [];
|
|
2450
|
+
for (const tk of tasks) {
|
|
2451
|
+
if (!tk.summary && tk.sourceTaskId) {
|
|
2452
|
+
const local = this.store.getTask(tk.sourceTaskId);
|
|
2453
|
+
if (local) {
|
|
2454
|
+
tk.summary = local.summary;
|
|
2455
|
+
tk.title = tk.title || local.title;
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
this.jsonResponse(res, { tasks });
|
|
2000
2460
|
}
|
|
2001
2461
|
catch (err) {
|
|
2002
2462
|
this.jsonResponse(res, { tasks: [], error: String(err) });
|
|
@@ -2015,13 +2475,55 @@ class ViewerServer {
|
|
|
2015
2475
|
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
2016
2476
|
}
|
|
2017
2477
|
}
|
|
2478
|
+
async serveHubTaskDetail(res, p) {
|
|
2479
|
+
const hub = this.resolveHubConnection();
|
|
2480
|
+
if (!hub)
|
|
2481
|
+
return this.jsonResponse(res, { error: "not_configured" }, 500);
|
|
2482
|
+
const m = p.match(/^\/api\/admin\/shared-tasks\/([^/]+)\/detail$/);
|
|
2483
|
+
if (!m)
|
|
2484
|
+
return this.jsonResponse(res, { error: "bad_request" }, 400);
|
|
2485
|
+
const taskId = decodeURIComponent(m[1]);
|
|
2486
|
+
try {
|
|
2487
|
+
const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/shared-tasks/${encodeURIComponent(taskId)}/detail`, { method: "GET" });
|
|
2488
|
+
this.jsonResponse(res, data);
|
|
2489
|
+
}
|
|
2490
|
+
catch (err) {
|
|
2491
|
+
this.jsonResponse(res, { error: String(err) }, 500);
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
async serveHubSkillDetail(res, p) {
|
|
2495
|
+
const hub = this.resolveHubConnection();
|
|
2496
|
+
if (!hub)
|
|
2497
|
+
return this.jsonResponse(res, { error: "not_configured" }, 500);
|
|
2498
|
+
const m = p.match(/^\/api\/admin\/shared-skills\/([^/]+)\/detail$/);
|
|
2499
|
+
if (!m)
|
|
2500
|
+
return this.jsonResponse(res, { error: "bad_request" }, 400);
|
|
2501
|
+
const skillId = decodeURIComponent(m[1]);
|
|
2502
|
+
try {
|
|
2503
|
+
const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/shared-skills/${encodeURIComponent(skillId)}/detail`, { method: "GET" });
|
|
2504
|
+
this.jsonResponse(res, data);
|
|
2505
|
+
}
|
|
2506
|
+
catch (err) {
|
|
2507
|
+
this.jsonResponse(res, { error: String(err) }, 500);
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2018
2510
|
async serveAdminSharedSkills(res) {
|
|
2019
2511
|
const hub = this.resolveHubConnection();
|
|
2020
2512
|
if (!hub)
|
|
2021
2513
|
return this.jsonResponse(res, { skills: [], error: "not_configured" });
|
|
2022
2514
|
try {
|
|
2023
2515
|
const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/shared-skills", { method: "GET" });
|
|
2024
|
-
|
|
2516
|
+
const skills = Array.isArray(data?.skills) ? data.skills : [];
|
|
2517
|
+
for (const sk of skills) {
|
|
2518
|
+
if (!sk.description && sk.sourceSkillId) {
|
|
2519
|
+
const local = this.store.getSkill(sk.sourceSkillId);
|
|
2520
|
+
if (local) {
|
|
2521
|
+
sk.description = sk.description || local.description;
|
|
2522
|
+
sk.name = sk.name || local.name;
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
this.jsonResponse(res, { skills });
|
|
2025
2527
|
}
|
|
2026
2528
|
catch (err) {
|
|
2027
2529
|
this.jsonResponse(res, { skills: [], error: String(err) });
|
|
@@ -2046,7 +2548,18 @@ class ViewerServer {
|
|
|
2046
2548
|
return this.jsonResponse(res, { memories: [], error: "not_configured" });
|
|
2047
2549
|
try {
|
|
2048
2550
|
const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/shared-memories", { method: "GET" });
|
|
2049
|
-
|
|
2551
|
+
const memories = Array.isArray(data?.memories) ? data.memories : [];
|
|
2552
|
+
for (const m of memories) {
|
|
2553
|
+
if (!m.content && m.sourceChunkId) {
|
|
2554
|
+
const local = this.store.getChunk(m.sourceChunkId);
|
|
2555
|
+
if (local) {
|
|
2556
|
+
m.content = local.content;
|
|
2557
|
+
if (!m.summary && local.summary)
|
|
2558
|
+
m.summary = local.summary;
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
this.jsonResponse(res, { memories });
|
|
2050
2563
|
}
|
|
2051
2564
|
catch (err) {
|
|
2052
2565
|
this.jsonResponse(res, { memories: [], error: String(err) });
|
|
@@ -2065,6 +2578,135 @@ class ViewerServer {
|
|
|
2065
2578
|
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
2066
2579
|
}
|
|
2067
2580
|
}
|
|
2581
|
+
async serveSharingNotifications(res, url) {
|
|
2582
|
+
const hub = this.resolveHubConnection();
|
|
2583
|
+
if (!hub)
|
|
2584
|
+
return this.jsonResponse(res, { notifications: [], unreadCount: 0 });
|
|
2585
|
+
try {
|
|
2586
|
+
const unread = url.searchParams.get("unread") === "1" ? "?unread=1" : "";
|
|
2587
|
+
const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/notifications${unread}`);
|
|
2588
|
+
this.jsonResponse(res, data);
|
|
2589
|
+
}
|
|
2590
|
+
catch {
|
|
2591
|
+
this.jsonResponse(res, { notifications: [], unreadCount: 0 });
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
handleSharingNotificationsRead(req, res) {
|
|
2595
|
+
const hub = this.resolveHubConnection();
|
|
2596
|
+
if (!hub)
|
|
2597
|
+
return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
2598
|
+
this.readBody(req, async (raw) => {
|
|
2599
|
+
try {
|
|
2600
|
+
const body = JSON.parse(raw || "{}");
|
|
2601
|
+
await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications/read", { method: "POST", body: JSON.stringify(body) });
|
|
2602
|
+
this.jsonResponse(res, { ok: true });
|
|
2603
|
+
try {
|
|
2604
|
+
const data = (await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications?unread=1"));
|
|
2605
|
+
const count = data?.unreadCount ?? 0;
|
|
2606
|
+
this.lastKnownNotifCount = count;
|
|
2607
|
+
this.broadcastNotifSSE({ type: "update", unreadCount: count });
|
|
2608
|
+
}
|
|
2609
|
+
catch { /* best effort */ }
|
|
2610
|
+
}
|
|
2611
|
+
catch (err) {
|
|
2612
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
2613
|
+
}
|
|
2614
|
+
});
|
|
2615
|
+
}
|
|
2616
|
+
handleSharingNotificationsClear(req, res) {
|
|
2617
|
+
const hub = this.resolveHubConnection();
|
|
2618
|
+
if (!hub)
|
|
2619
|
+
return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
2620
|
+
this.readBody(req, async () => {
|
|
2621
|
+
try {
|
|
2622
|
+
await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications/clear", { method: "POST", body: "{}" });
|
|
2623
|
+
this.jsonResponse(res, { ok: true });
|
|
2624
|
+
this.broadcastNotifSSE({ type: "cleared", unreadCount: 0 });
|
|
2625
|
+
}
|
|
2626
|
+
catch (err) {
|
|
2627
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
2628
|
+
}
|
|
2629
|
+
});
|
|
2630
|
+
}
|
|
2631
|
+
handleNotifSSE(req, res) {
|
|
2632
|
+
res.writeHead(200, {
|
|
2633
|
+
"Content-Type": "text/event-stream",
|
|
2634
|
+
"Cache-Control": "no-cache",
|
|
2635
|
+
Connection: "keep-alive",
|
|
2636
|
+
"Access-Control-Allow-Origin": "*",
|
|
2637
|
+
});
|
|
2638
|
+
res.write("data: {\"type\":\"connected\"}\n\n");
|
|
2639
|
+
this.notifSSEClients.push(res);
|
|
2640
|
+
if (!this.notifPollTimer)
|
|
2641
|
+
this.startNotifPoll();
|
|
2642
|
+
req.on("close", () => {
|
|
2643
|
+
this.notifSSEClients = this.notifSSEClients.filter((c) => c !== res);
|
|
2644
|
+
if (this.notifSSEClients.length === 0)
|
|
2645
|
+
this.stopNotifPoll();
|
|
2646
|
+
});
|
|
2647
|
+
}
|
|
2648
|
+
broadcastNotifSSE(data) {
|
|
2649
|
+
const msg = `data: ${JSON.stringify(data)}\n\n`;
|
|
2650
|
+
this.notifSSEClients = this.notifSSEClients.filter((c) => {
|
|
2651
|
+
try {
|
|
2652
|
+
c.write(msg);
|
|
2653
|
+
return true;
|
|
2654
|
+
}
|
|
2655
|
+
catch {
|
|
2656
|
+
return false;
|
|
2657
|
+
}
|
|
2658
|
+
});
|
|
2659
|
+
}
|
|
2660
|
+
startNotifPoll() {
|
|
2661
|
+
this.stopNotifPoll();
|
|
2662
|
+
const tick = async () => {
|
|
2663
|
+
const hub = this.resolveHubConnection();
|
|
2664
|
+
if (!hub)
|
|
2665
|
+
return;
|
|
2666
|
+
try {
|
|
2667
|
+
const data = (await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications?unread=1"));
|
|
2668
|
+
const count = data?.unreadCount ?? 0;
|
|
2669
|
+
if (count !== this.lastKnownNotifCount) {
|
|
2670
|
+
this.lastKnownNotifCount = count;
|
|
2671
|
+
this.broadcastNotifSSE({ type: "update", unreadCount: count });
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
catch { /* ignore */ }
|
|
2675
|
+
};
|
|
2676
|
+
tick();
|
|
2677
|
+
this.notifPollTimer = setInterval(tick, 3000);
|
|
2678
|
+
}
|
|
2679
|
+
stopNotifPoll() {
|
|
2680
|
+
if (this.notifPollTimer) {
|
|
2681
|
+
clearInterval(this.notifPollTimer);
|
|
2682
|
+
this.notifPollTimer = undefined;
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
startHubHeartbeat() {
|
|
2686
|
+
this.stopHubHeartbeat();
|
|
2687
|
+
const sendHeartbeat = async () => {
|
|
2688
|
+
try {
|
|
2689
|
+
const hub = this.resolveHubConnection();
|
|
2690
|
+
if (!hub) {
|
|
2691
|
+
const persisted = this.store.getClientHubConnection();
|
|
2692
|
+
if (persisted?.hubUrl && persisted?.userToken) {
|
|
2693
|
+
await (0, hub_1.hubRequestJson)(persisted.hubUrl, persisted.userToken, "/api/v1/hub/heartbeat", { method: "POST" });
|
|
2694
|
+
}
|
|
2695
|
+
return;
|
|
2696
|
+
}
|
|
2697
|
+
await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/heartbeat", { method: "POST" });
|
|
2698
|
+
}
|
|
2699
|
+
catch { /* best-effort */ }
|
|
2700
|
+
};
|
|
2701
|
+
sendHeartbeat();
|
|
2702
|
+
this.hubHeartbeatTimer = setInterval(sendHeartbeat, ViewerServer.HUB_HEARTBEAT_INTERVAL_MS);
|
|
2703
|
+
}
|
|
2704
|
+
stopHubHeartbeat() {
|
|
2705
|
+
if (this.hubHeartbeatTimer) {
|
|
2706
|
+
clearInterval(this.hubHeartbeatTimer);
|
|
2707
|
+
this.hubHeartbeatTimer = undefined;
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2068
2710
|
getLocalIPs() {
|
|
2069
2711
|
const nets = node_os_1.default.networkInterfaces();
|
|
2070
2712
|
const ips = [];
|
|
@@ -2116,7 +2758,7 @@ class ViewerServer {
|
|
|
2116
2758
|
}
|
|
2117
2759
|
}
|
|
2118
2760
|
handleSaveConfig(req, res) {
|
|
2119
|
-
this.readBody(req, (body) => {
|
|
2761
|
+
this.readBody(req, async (body) => {
|
|
2120
2762
|
try {
|
|
2121
2763
|
const newCfg = JSON.parse(body);
|
|
2122
2764
|
const cfgPath = this.getOpenClawConfigPath();
|
|
@@ -2141,6 +2783,10 @@ class ViewerServer {
|
|
|
2141
2783
|
if (!entry.config)
|
|
2142
2784
|
entry.config = {};
|
|
2143
2785
|
const config = entry.config;
|
|
2786
|
+
const oldSharing = config.sharing;
|
|
2787
|
+
const oldSharingRole = oldSharing?.role;
|
|
2788
|
+
const oldSharingEnabled = Boolean(oldSharing?.enabled);
|
|
2789
|
+
const oldClientHubAddress = String(oldSharing?.client?.hubAddress || "");
|
|
2144
2790
|
if (newCfg.embedding)
|
|
2145
2791
|
config.embedding = newCfg.embedding;
|
|
2146
2792
|
if (newCfg.summarizer)
|
|
@@ -2174,6 +2820,33 @@ class ViewerServer {
|
|
|
2174
2820
|
catch { }
|
|
2175
2821
|
}
|
|
2176
2822
|
}
|
|
2823
|
+
const newRole = merged.role;
|
|
2824
|
+
const newEnabled = Boolean(merged.enabled);
|
|
2825
|
+
// Detect disabling sharing or switching away from hub mode
|
|
2826
|
+
const wasHub = oldSharingEnabled && oldSharingRole === "hub";
|
|
2827
|
+
const isHub = newEnabled && newRole === "hub";
|
|
2828
|
+
if (wasHub && !isHub) {
|
|
2829
|
+
await this.notifyHubShutdown();
|
|
2830
|
+
this.stopHubHeartbeat();
|
|
2831
|
+
this.log.info("Hub shutting down: notified connected clients");
|
|
2832
|
+
}
|
|
2833
|
+
// Detect disabling sharing or switching away from client mode
|
|
2834
|
+
const wasClient = oldSharingEnabled && oldSharingRole === "client";
|
|
2835
|
+
const isClient = newEnabled && newRole === "client";
|
|
2836
|
+
if (wasClient && !isClient) {
|
|
2837
|
+
this.notifyHubLeave();
|
|
2838
|
+
this.store.clearClientHubConnection();
|
|
2839
|
+
this.log.info("Cleared client hub connection (sharing disabled or role changed)");
|
|
2840
|
+
}
|
|
2841
|
+
// Detect switching to a different Hub while still in client mode
|
|
2842
|
+
if (wasClient && isClient) {
|
|
2843
|
+
const newClientAddr = String(merged.client?.hubAddress || "");
|
|
2844
|
+
if (newClientAddr && oldClientHubAddress && (0, hub_1.normalizeHubUrl)(newClientAddr) !== (0, hub_1.normalizeHubUrl)(oldClientHubAddress)) {
|
|
2845
|
+
this.notifyHubLeave();
|
|
2846
|
+
this.store.clearClientHubConnection();
|
|
2847
|
+
this.log.info("Cleared client hub connection (switched to different Hub)");
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2177
2850
|
if (merged.role === "hub") {
|
|
2178
2851
|
merged.client = { hubAddress: "", userToken: "", teamToken: "" };
|
|
2179
2852
|
}
|
|
@@ -2185,6 +2858,12 @@ class ViewerServer {
|
|
|
2185
2858
|
node_fs_1.default.mkdirSync(node_path_1.default.dirname(cfgPath), { recursive: true });
|
|
2186
2859
|
node_fs_1.default.writeFileSync(cfgPath, JSON.stringify(raw, null, 2), "utf-8");
|
|
2187
2860
|
this.log.info("Plugin config updated via Viewer");
|
|
2861
|
+
this.stopHubHeartbeat();
|
|
2862
|
+
// When switching to client mode, immediately send join request
|
|
2863
|
+
const finalSharing = config.sharing;
|
|
2864
|
+
if (finalSharing?.role === "client" && oldSharingRole !== "client") {
|
|
2865
|
+
this.autoJoinOnSave(finalSharing).catch(e => this.log.warn(`Auto-join on save failed: ${e}`));
|
|
2866
|
+
}
|
|
2188
2867
|
this.jsonResponse(res, { ok: true });
|
|
2189
2868
|
}
|
|
2190
2869
|
catch (e) {
|
|
@@ -2194,6 +2873,92 @@ class ViewerServer {
|
|
|
2194
2873
|
}
|
|
2195
2874
|
});
|
|
2196
2875
|
}
|
|
2876
|
+
async autoJoinOnSave(sharing) {
|
|
2877
|
+
const clientCfg = sharing.client;
|
|
2878
|
+
const hubAddress = String(clientCfg?.hubAddress || "");
|
|
2879
|
+
const teamToken = String(clientCfg?.teamToken || "");
|
|
2880
|
+
if (!hubAddress || !teamToken)
|
|
2881
|
+
return;
|
|
2882
|
+
const hubUrl = (0, hub_1.normalizeHubUrl)(hubAddress);
|
|
2883
|
+
const os = await Promise.resolve().then(() => __importStar(require("os")));
|
|
2884
|
+
const nickname = String(clientCfg?.nickname || "");
|
|
2885
|
+
const username = nickname || os.userInfo().username || "user";
|
|
2886
|
+
const hostname = os.hostname() || "unknown";
|
|
2887
|
+
const result = await (0, hub_1.hubRequestJson)(hubUrl, "", "/api/v1/hub/join", {
|
|
2888
|
+
method: "POST",
|
|
2889
|
+
body: JSON.stringify({ teamToken, username, deviceName: hostname }),
|
|
2890
|
+
});
|
|
2891
|
+
this.store.setClientHubConnection({
|
|
2892
|
+
hubUrl,
|
|
2893
|
+
userId: String(result.userId || ""),
|
|
2894
|
+
username,
|
|
2895
|
+
userToken: result.userToken || "",
|
|
2896
|
+
role: "member",
|
|
2897
|
+
connectedAt: Date.now(),
|
|
2898
|
+
});
|
|
2899
|
+
this.log.info(`Auto-join on save: status=${result.status}, userId=${result.userId}`);
|
|
2900
|
+
if (result.userToken) {
|
|
2901
|
+
this.startHubHeartbeat();
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
async notifyHubLeave() {
|
|
2905
|
+
try {
|
|
2906
|
+
const hub = this.resolveHubConnection();
|
|
2907
|
+
if (hub) {
|
|
2908
|
+
await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/leave", { method: "POST" });
|
|
2909
|
+
this.log.info("Notified Hub of voluntary leave");
|
|
2910
|
+
return;
|
|
2911
|
+
}
|
|
2912
|
+
const persisted = this.store.getClientHubConnection();
|
|
2913
|
+
if (persisted?.hubUrl && persisted?.userToken) {
|
|
2914
|
+
await (0, hub_1.hubRequestJson)(persisted.hubUrl, persisted.userToken, "/api/v1/hub/leave", { method: "POST" });
|
|
2915
|
+
this.log.info("Notified Hub of voluntary leave (persisted connection)");
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
catch (e) {
|
|
2919
|
+
this.log.warn(`Failed to notify Hub of leave: ${e}`);
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
async notifyHubShutdown() {
|
|
2923
|
+
try {
|
|
2924
|
+
const sharing = this.ctx?.config.sharing;
|
|
2925
|
+
if (!sharing || sharing.role !== "hub")
|
|
2926
|
+
return;
|
|
2927
|
+
const hubPort = sharing.hub?.port ?? 18800;
|
|
2928
|
+
const authPath = node_path_1.default.join(this.dataDir, "hub-auth.json");
|
|
2929
|
+
let adminToken;
|
|
2930
|
+
try {
|
|
2931
|
+
const authData = JSON.parse(node_fs_1.default.readFileSync(authPath, "utf8"));
|
|
2932
|
+
adminToken = authData?.bootstrapAdminToken;
|
|
2933
|
+
}
|
|
2934
|
+
catch {
|
|
2935
|
+
return;
|
|
2936
|
+
}
|
|
2937
|
+
if (!adminToken)
|
|
2938
|
+
return;
|
|
2939
|
+
const users = this.store.listHubUsers("active");
|
|
2940
|
+
const { v4: uuidv4 } = require("uuid");
|
|
2941
|
+
for (const u of users) {
|
|
2942
|
+
try {
|
|
2943
|
+
this.store.insertHubNotification({
|
|
2944
|
+
id: uuidv4(),
|
|
2945
|
+
userId: u.id,
|
|
2946
|
+
type: "hub_shutdown",
|
|
2947
|
+
resource: "hub",
|
|
2948
|
+
title: "Hub is shutting down",
|
|
2949
|
+
message: "The Hub server is shutting down. You may be disconnected.",
|
|
2950
|
+
});
|
|
2951
|
+
}
|
|
2952
|
+
catch (e) {
|
|
2953
|
+
this.log.warn(`Failed to insert shutdown notification for user ${u.id}: ${e}`);
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2956
|
+
this.log.info(`Hub shutdown: notified ${users.length} approved user(s)`);
|
|
2957
|
+
}
|
|
2958
|
+
catch (e) {
|
|
2959
|
+
this.log.warn(`notifyHubShutdown error: ${e}`);
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2197
2962
|
handleUpdateUsername(req, res) {
|
|
2198
2963
|
this.readBody(req, async (body) => {
|
|
2199
2964
|
if (!this.ctx)
|
|
@@ -2668,7 +3433,7 @@ class ViewerServer {
|
|
|
2668
3433
|
// ─── Migration: scan OpenClaw built-in memory ───
|
|
2669
3434
|
getOpenClawHome() {
|
|
2670
3435
|
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
2671
|
-
return node_path_1.default.join(home, ".openclaw");
|
|
3436
|
+
return process.env.OPENCLAW_STATE_DIR || node_path_1.default.join(home, ".openclaw");
|
|
2672
3437
|
}
|
|
2673
3438
|
handleCleanupPolluted(res) {
|
|
2674
3439
|
try {
|
|
@@ -2696,7 +3461,7 @@ class ViewerServer {
|
|
|
2696
3461
|
try {
|
|
2697
3462
|
const ocHome = this.getOpenClawHome();
|
|
2698
3463
|
const memoryDir = node_path_1.default.join(ocHome, "memory");
|
|
2699
|
-
const
|
|
3464
|
+
const agentsDir = node_path_1.default.join(ocHome, "agents");
|
|
2700
3465
|
const sqliteFiles = [];
|
|
2701
3466
|
if (node_fs_1.default.existsSync(memoryDir)) {
|
|
2702
3467
|
for (const f of node_fs_1.default.readdirSync(memoryDir)) {
|
|
@@ -2714,38 +3479,45 @@ class ViewerServer {
|
|
|
2714
3479
|
}
|
|
2715
3480
|
let sessionCount = 0;
|
|
2716
3481
|
let messageCount = 0;
|
|
2717
|
-
if (node_fs_1.default.existsSync(
|
|
2718
|
-
const
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
txt =
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
3482
|
+
if (node_fs_1.default.existsSync(agentsDir)) {
|
|
3483
|
+
for (const entry of node_fs_1.default.readdirSync(agentsDir, { withFileTypes: true })) {
|
|
3484
|
+
if (!entry.isDirectory())
|
|
3485
|
+
continue;
|
|
3486
|
+
const sessDir = node_path_1.default.join(agentsDir, entry.name, "sessions");
|
|
3487
|
+
if (!node_fs_1.default.existsSync(sessDir))
|
|
3488
|
+
continue;
|
|
3489
|
+
const jsonlFiles = node_fs_1.default.readdirSync(sessDir).filter(f => f.includes(".jsonl"));
|
|
3490
|
+
sessionCount += jsonlFiles.length;
|
|
3491
|
+
for (const f of jsonlFiles) {
|
|
3492
|
+
try {
|
|
3493
|
+
const content = node_fs_1.default.readFileSync(node_path_1.default.join(sessDir, f), "utf-8");
|
|
3494
|
+
const lines = content.split("\n").filter(l => l.trim());
|
|
3495
|
+
for (const line of lines) {
|
|
3496
|
+
try {
|
|
3497
|
+
const obj = JSON.parse(line);
|
|
3498
|
+
if (obj.type === "message") {
|
|
3499
|
+
const role = obj.message?.role ?? obj.role;
|
|
3500
|
+
if (role === "user" || role === "assistant") {
|
|
3501
|
+
const mc = obj.message?.content ?? obj.content;
|
|
3502
|
+
let txt = "";
|
|
3503
|
+
if (typeof mc === "string")
|
|
3504
|
+
txt = mc;
|
|
3505
|
+
else if (Array.isArray(mc))
|
|
3506
|
+
txt = mc.filter((p) => p.type === "text" && p.text).map((p) => p.text).join("\n");
|
|
3507
|
+
else
|
|
3508
|
+
txt = JSON.stringify(mc);
|
|
3509
|
+
if (role === "user")
|
|
3510
|
+
txt = (0, capture_1.stripInboundMetadata)(txt);
|
|
3511
|
+
if (txt && txt.length >= 10)
|
|
3512
|
+
messageCount++;
|
|
3513
|
+
}
|
|
2742
3514
|
}
|
|
2743
3515
|
}
|
|
3516
|
+
catch { /* skip bad lines */ }
|
|
2744
3517
|
}
|
|
2745
|
-
catch { /* skip bad lines */ }
|
|
2746
3518
|
}
|
|
3519
|
+
catch { /* skip unreadable */ }
|
|
2747
3520
|
}
|
|
2748
|
-
catch { /* skip unreadable */ }
|
|
2749
3521
|
}
|
|
2750
3522
|
}
|
|
2751
3523
|
const cfgPath = this.getOpenClawConfigPath();
|