@memtensor/memos-local-openclaw-plugin 1.0.4-beta.1 → 1.0.4-beta.11
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 +2 -2
- package/dist/client/connector.d.ts.map +1 -1
- package/dist/client/connector.js +122 -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 +8 -0
- package/dist/hub/server.d.ts.map +1 -1
- package/dist/hub/server.js +390 -106
- package/dist/hub/server.js.map +1 -1
- package/dist/hub/user-manager.d.ts +11 -0
- package/dist/hub/user-manager.d.ts.map +1 -1
- package/dist/hub/user-manager.js +31 -3
- 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 +93 -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 +4 -0
- package/dist/skill/evolver.d.ts.map +1 -1
- package/dist/skill/evolver.js +59 -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/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 +89 -20
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +374 -124
- 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 +11 -2
- 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 +2671 -879
- 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 +990 -198
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +700 -56
- 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 +124 -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 +374 -97
- package/src/hub/user-manager.ts +48 -8
- package/src/index.ts +10 -2
- package/src/ingest/providers/index.ts +41 -7
- package/src/recall/engine.ts +86 -1
- package/src/shared/llm-call.ts +97 -9
- package/src/sharing/types.ts +1 -1
- package/src/skill/evolver.ts +63 -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/ensure-binding.ts +52 -0
- package/src/storage/sqlite.ts +395 -148
- package/src/telemetry.ts +172 -41
- package/src/tools/memory-search.ts +2 -1
- package/src/types.ts +12 -2
- package/src/viewer/html.ts +2671 -879
- package/src/viewer/server.ts +913 -182
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,13 +1925,27 @@ 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
|
+
const targetPort = u.port || (u.protocol === "https:" ? "443" : "80");
|
|
1933
|
+
if (localIPs.includes(u.hostname) && targetPort === String(this.port)) {
|
|
1934
|
+
return this.jsonResponse(res, { ok: false, error: "cannot_join_self" });
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
catch { }
|
|
1401
1938
|
const os = await Promise.resolve().then(() => __importStar(require("os")));
|
|
1402
|
-
const
|
|
1939
|
+
const nickname = sharing.client?.nickname;
|
|
1940
|
+
const username = nickname || os.userInfo().username || "user";
|
|
1403
1941
|
const hostname = os.hostname() || "unknown";
|
|
1942
|
+
const persisted = this.store.getClientHubConnection();
|
|
1943
|
+
const existingIdentityKey = persisted?.identityKey || "";
|
|
1404
1944
|
const result = await (0, hub_1.hubRequestJson)(hubUrl, "", "/api/v1/hub/join", {
|
|
1405
1945
|
method: "POST",
|
|
1406
|
-
body: JSON.stringify({ teamToken, username, deviceName: hostname }),
|
|
1946
|
+
body: JSON.stringify({ teamToken, username, deviceName: hostname, reapply: true, identityKey: existingIdentityKey }),
|
|
1407
1947
|
});
|
|
1948
|
+
const returnedIdentityKey = String(result.identityKey || existingIdentityKey || "");
|
|
1408
1949
|
this.store.setClientHubConnection({
|
|
1409
1950
|
hubUrl,
|
|
1410
1951
|
userId: String(result.userId || ""),
|
|
@@ -1412,6 +1953,8 @@ class ViewerServer {
|
|
|
1412
1953
|
userToken: result.userToken || "",
|
|
1413
1954
|
role: "member",
|
|
1414
1955
|
connectedAt: Date.now(),
|
|
1956
|
+
identityKey: returnedIdentityKey,
|
|
1957
|
+
lastKnownStatus: result.status || "",
|
|
1415
1958
|
});
|
|
1416
1959
|
this.jsonResponse(res, { ok: true, status: result.status || "pending" });
|
|
1417
1960
|
}
|
|
@@ -1425,7 +1968,14 @@ class ViewerServer {
|
|
|
1425
1968
|
return this.jsonResponse(res, { memories: [], error: "sharing_unavailable" });
|
|
1426
1969
|
try {
|
|
1427
1970
|
const limit = Number(url.searchParams.get("limit") || 40);
|
|
1428
|
-
const
|
|
1971
|
+
const hub = this.resolveHubConnection();
|
|
1972
|
+
let data;
|
|
1973
|
+
if (hub) {
|
|
1974
|
+
data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/memories?limit=${limit}`);
|
|
1975
|
+
}
|
|
1976
|
+
else {
|
|
1977
|
+
data = await (0, hub_1.hubListMemories)(this.store, this.ctx, { limit });
|
|
1978
|
+
}
|
|
1429
1979
|
this.jsonResponse(res, { memories: Array.isArray(data?.memories) ? data.memories : [] });
|
|
1430
1980
|
}
|
|
1431
1981
|
catch (err) {
|
|
@@ -1437,7 +1987,14 @@ class ViewerServer {
|
|
|
1437
1987
|
return this.jsonResponse(res, { tasks: [], error: "sharing_unavailable" });
|
|
1438
1988
|
try {
|
|
1439
1989
|
const limit = Number(url.searchParams.get("limit") || 40);
|
|
1440
|
-
const
|
|
1990
|
+
const hub = this.resolveHubConnection();
|
|
1991
|
+
let data;
|
|
1992
|
+
if (hub) {
|
|
1993
|
+
data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/tasks?limit=${limit}`);
|
|
1994
|
+
}
|
|
1995
|
+
else {
|
|
1996
|
+
data = await (0, hub_1.hubListTasks)(this.store, this.ctx, { limit });
|
|
1997
|
+
}
|
|
1441
1998
|
this.jsonResponse(res, { tasks: Array.isArray(data?.tasks) ? data.tasks : [] });
|
|
1442
1999
|
}
|
|
1443
2000
|
catch (err) {
|
|
@@ -1449,7 +2006,14 @@ class ViewerServer {
|
|
|
1449
2006
|
return this.jsonResponse(res, { skills: [], error: "sharing_unavailable" });
|
|
1450
2007
|
try {
|
|
1451
2008
|
const limit = Number(url.searchParams.get("limit") || 40);
|
|
1452
|
-
const
|
|
2009
|
+
const hub = this.resolveHubConnection();
|
|
2010
|
+
let data;
|
|
2011
|
+
if (hub) {
|
|
2012
|
+
data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/skills/list?limit=${limit}`);
|
|
2013
|
+
}
|
|
2014
|
+
else {
|
|
2015
|
+
data = await (0, hub_1.hubListSkills)(this.store, this.ctx, { limit });
|
|
2016
|
+
}
|
|
1453
2017
|
this.jsonResponse(res, { skills: Array.isArray(data?.skills) ? data.skills : [] });
|
|
1454
2018
|
}
|
|
1455
2019
|
catch (err) {
|
|
@@ -1466,13 +2030,22 @@ class ViewerServer {
|
|
|
1466
2030
|
const query = String(parsed.query || "");
|
|
1467
2031
|
const role = typeof parsed.role === "string" ? parsed.role : undefined;
|
|
1468
2032
|
const maxResults = typeof parsed.maxResults === "number" ? parsed.maxResults : 10;
|
|
1469
|
-
const scope = parsed.scope === "group" || parsed.scope === "all" ? parsed.scope : "local";
|
|
2033
|
+
const scope = parsed.scope === "group" || parsed.scope === "all" || parsed.scope === "hub" ? (parsed.scope === "hub" ? "all" : parsed.scope) : "local";
|
|
1470
2034
|
const local = this.searchLocalViewerMemories(query, { role, maxResults });
|
|
1471
2035
|
if (scope === "local") {
|
|
1472
2036
|
return this.jsonResponse(res, { local: { hits: local.hits, meta: local.meta }, hub: emptyHub });
|
|
1473
2037
|
}
|
|
1474
2038
|
try {
|
|
1475
|
-
const
|
|
2039
|
+
const conn = this.resolveHubConnection();
|
|
2040
|
+
let hub;
|
|
2041
|
+
if (conn) {
|
|
2042
|
+
hub = await (0, hub_1.hubRequestJson)(conn.hubUrl, conn.userToken, "/api/v1/hub/search", {
|
|
2043
|
+
method: "POST", body: JSON.stringify({ query, maxResults, scope }),
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
else {
|
|
2047
|
+
hub = await (0, hub_1.hubSearchMemories)(this.store, this.ctx, { query, maxResults, scope });
|
|
2048
|
+
}
|
|
1476
2049
|
this.jsonResponse(res, { local: { hits: local.hits, meta: local.meta }, hub });
|
|
1477
2050
|
}
|
|
1478
2051
|
catch (err) {
|
|
@@ -1860,123 +2433,6 @@ class ViewerServer {
|
|
|
1860
2433
|
}
|
|
1861
2434
|
return (0, hub_1.resolveHubClient)(this.store, this.ctx);
|
|
1862
2435
|
}
|
|
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
2436
|
async serveSharingUsers(res) {
|
|
1981
2437
|
const hub = this.resolveHubConnection();
|
|
1982
2438
|
if (!hub)
|
|
@@ -1996,7 +2452,17 @@ class ViewerServer {
|
|
|
1996
2452
|
return this.jsonResponse(res, { tasks: [], error: "not_configured" });
|
|
1997
2453
|
try {
|
|
1998
2454
|
const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/shared-tasks", { method: "GET" });
|
|
1999
|
-
|
|
2455
|
+
const tasks = Array.isArray(data?.tasks) ? data.tasks : [];
|
|
2456
|
+
for (const tk of tasks) {
|
|
2457
|
+
if (!tk.summary && tk.sourceTaskId) {
|
|
2458
|
+
const local = this.store.getTask(tk.sourceTaskId);
|
|
2459
|
+
if (local) {
|
|
2460
|
+
tk.summary = local.summary;
|
|
2461
|
+
tk.title = tk.title || local.title;
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
this.jsonResponse(res, { tasks });
|
|
2000
2466
|
}
|
|
2001
2467
|
catch (err) {
|
|
2002
2468
|
this.jsonResponse(res, { tasks: [], error: String(err) });
|
|
@@ -2015,13 +2481,55 @@ class ViewerServer {
|
|
|
2015
2481
|
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
2016
2482
|
}
|
|
2017
2483
|
}
|
|
2484
|
+
async serveHubTaskDetail(res, p) {
|
|
2485
|
+
const hub = this.resolveHubConnection();
|
|
2486
|
+
if (!hub)
|
|
2487
|
+
return this.jsonResponse(res, { error: "not_configured" }, 500);
|
|
2488
|
+
const m = p.match(/^\/api\/admin\/shared-tasks\/([^/]+)\/detail$/);
|
|
2489
|
+
if (!m)
|
|
2490
|
+
return this.jsonResponse(res, { error: "bad_request" }, 400);
|
|
2491
|
+
const taskId = decodeURIComponent(m[1]);
|
|
2492
|
+
try {
|
|
2493
|
+
const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/shared-tasks/${encodeURIComponent(taskId)}/detail`, { method: "GET" });
|
|
2494
|
+
this.jsonResponse(res, data);
|
|
2495
|
+
}
|
|
2496
|
+
catch (err) {
|
|
2497
|
+
this.jsonResponse(res, { error: String(err) }, 500);
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
async serveHubSkillDetail(res, p) {
|
|
2501
|
+
const hub = this.resolveHubConnection();
|
|
2502
|
+
if (!hub)
|
|
2503
|
+
return this.jsonResponse(res, { error: "not_configured" }, 500);
|
|
2504
|
+
const m = p.match(/^\/api\/admin\/shared-skills\/([^/]+)\/detail$/);
|
|
2505
|
+
if (!m)
|
|
2506
|
+
return this.jsonResponse(res, { error: "bad_request" }, 400);
|
|
2507
|
+
const skillId = decodeURIComponent(m[1]);
|
|
2508
|
+
try {
|
|
2509
|
+
const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/shared-skills/${encodeURIComponent(skillId)}/detail`, { method: "GET" });
|
|
2510
|
+
this.jsonResponse(res, data);
|
|
2511
|
+
}
|
|
2512
|
+
catch (err) {
|
|
2513
|
+
this.jsonResponse(res, { error: String(err) }, 500);
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2018
2516
|
async serveAdminSharedSkills(res) {
|
|
2019
2517
|
const hub = this.resolveHubConnection();
|
|
2020
2518
|
if (!hub)
|
|
2021
2519
|
return this.jsonResponse(res, { skills: [], error: "not_configured" });
|
|
2022
2520
|
try {
|
|
2023
2521
|
const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/shared-skills", { method: "GET" });
|
|
2024
|
-
|
|
2522
|
+
const skills = Array.isArray(data?.skills) ? data.skills : [];
|
|
2523
|
+
for (const sk of skills) {
|
|
2524
|
+
if (!sk.description && sk.sourceSkillId) {
|
|
2525
|
+
const local = this.store.getSkill(sk.sourceSkillId);
|
|
2526
|
+
if (local) {
|
|
2527
|
+
sk.description = sk.description || local.description;
|
|
2528
|
+
sk.name = sk.name || local.name;
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
this.jsonResponse(res, { skills });
|
|
2025
2533
|
}
|
|
2026
2534
|
catch (err) {
|
|
2027
2535
|
this.jsonResponse(res, { skills: [], error: String(err) });
|
|
@@ -2046,7 +2554,18 @@ class ViewerServer {
|
|
|
2046
2554
|
return this.jsonResponse(res, { memories: [], error: "not_configured" });
|
|
2047
2555
|
try {
|
|
2048
2556
|
const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/shared-memories", { method: "GET" });
|
|
2049
|
-
|
|
2557
|
+
const memories = Array.isArray(data?.memories) ? data.memories : [];
|
|
2558
|
+
for (const m of memories) {
|
|
2559
|
+
if (!m.content && m.sourceChunkId) {
|
|
2560
|
+
const local = this.store.getChunk(m.sourceChunkId);
|
|
2561
|
+
if (local) {
|
|
2562
|
+
m.content = local.content;
|
|
2563
|
+
if (!m.summary && local.summary)
|
|
2564
|
+
m.summary = local.summary;
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
this.jsonResponse(res, { memories });
|
|
2050
2569
|
}
|
|
2051
2570
|
catch (err) {
|
|
2052
2571
|
this.jsonResponse(res, { memories: [], error: String(err) });
|
|
@@ -2065,6 +2584,135 @@ class ViewerServer {
|
|
|
2065
2584
|
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
2066
2585
|
}
|
|
2067
2586
|
}
|
|
2587
|
+
async serveSharingNotifications(res, url) {
|
|
2588
|
+
const hub = this.resolveHubConnection();
|
|
2589
|
+
if (!hub)
|
|
2590
|
+
return this.jsonResponse(res, { notifications: [], unreadCount: 0 });
|
|
2591
|
+
try {
|
|
2592
|
+
const unread = url.searchParams.get("unread") === "1" ? "?unread=1" : "";
|
|
2593
|
+
const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/notifications${unread}`);
|
|
2594
|
+
this.jsonResponse(res, data);
|
|
2595
|
+
}
|
|
2596
|
+
catch {
|
|
2597
|
+
this.jsonResponse(res, { notifications: [], unreadCount: 0 });
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
handleSharingNotificationsRead(req, res) {
|
|
2601
|
+
const hub = this.resolveHubConnection();
|
|
2602
|
+
if (!hub)
|
|
2603
|
+
return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
2604
|
+
this.readBody(req, async (raw) => {
|
|
2605
|
+
try {
|
|
2606
|
+
const body = JSON.parse(raw || "{}");
|
|
2607
|
+
await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications/read", { method: "POST", body: JSON.stringify(body) });
|
|
2608
|
+
this.jsonResponse(res, { ok: true });
|
|
2609
|
+
try {
|
|
2610
|
+
const data = (await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications?unread=1"));
|
|
2611
|
+
const count = data?.unreadCount ?? 0;
|
|
2612
|
+
this.lastKnownNotifCount = count;
|
|
2613
|
+
this.broadcastNotifSSE({ type: "update", unreadCount: count });
|
|
2614
|
+
}
|
|
2615
|
+
catch { /* best effort */ }
|
|
2616
|
+
}
|
|
2617
|
+
catch (err) {
|
|
2618
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
2619
|
+
}
|
|
2620
|
+
});
|
|
2621
|
+
}
|
|
2622
|
+
handleSharingNotificationsClear(req, res) {
|
|
2623
|
+
const hub = this.resolveHubConnection();
|
|
2624
|
+
if (!hub)
|
|
2625
|
+
return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
2626
|
+
this.readBody(req, async () => {
|
|
2627
|
+
try {
|
|
2628
|
+
await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications/clear", { method: "POST", body: "{}" });
|
|
2629
|
+
this.jsonResponse(res, { ok: true });
|
|
2630
|
+
this.broadcastNotifSSE({ type: "cleared", unreadCount: 0 });
|
|
2631
|
+
}
|
|
2632
|
+
catch (err) {
|
|
2633
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
2634
|
+
}
|
|
2635
|
+
});
|
|
2636
|
+
}
|
|
2637
|
+
handleNotifSSE(req, res) {
|
|
2638
|
+
res.writeHead(200, {
|
|
2639
|
+
"Content-Type": "text/event-stream",
|
|
2640
|
+
"Cache-Control": "no-cache",
|
|
2641
|
+
Connection: "keep-alive",
|
|
2642
|
+
"Access-Control-Allow-Origin": "*",
|
|
2643
|
+
});
|
|
2644
|
+
res.write("data: {\"type\":\"connected\"}\n\n");
|
|
2645
|
+
this.notifSSEClients.push(res);
|
|
2646
|
+
if (!this.notifPollTimer)
|
|
2647
|
+
this.startNotifPoll();
|
|
2648
|
+
req.on("close", () => {
|
|
2649
|
+
this.notifSSEClients = this.notifSSEClients.filter((c) => c !== res);
|
|
2650
|
+
if (this.notifSSEClients.length === 0)
|
|
2651
|
+
this.stopNotifPoll();
|
|
2652
|
+
});
|
|
2653
|
+
}
|
|
2654
|
+
broadcastNotifSSE(data) {
|
|
2655
|
+
const msg = `data: ${JSON.stringify(data)}\n\n`;
|
|
2656
|
+
this.notifSSEClients = this.notifSSEClients.filter((c) => {
|
|
2657
|
+
try {
|
|
2658
|
+
c.write(msg);
|
|
2659
|
+
return true;
|
|
2660
|
+
}
|
|
2661
|
+
catch {
|
|
2662
|
+
return false;
|
|
2663
|
+
}
|
|
2664
|
+
});
|
|
2665
|
+
}
|
|
2666
|
+
startNotifPoll() {
|
|
2667
|
+
this.stopNotifPoll();
|
|
2668
|
+
const tick = async () => {
|
|
2669
|
+
const hub = this.resolveHubConnection();
|
|
2670
|
+
if (!hub)
|
|
2671
|
+
return;
|
|
2672
|
+
try {
|
|
2673
|
+
const data = (await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications?unread=1"));
|
|
2674
|
+
const count = data?.unreadCount ?? 0;
|
|
2675
|
+
if (count !== this.lastKnownNotifCount) {
|
|
2676
|
+
this.lastKnownNotifCount = count;
|
|
2677
|
+
this.broadcastNotifSSE({ type: "update", unreadCount: count });
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
catch { /* ignore */ }
|
|
2681
|
+
};
|
|
2682
|
+
tick();
|
|
2683
|
+
this.notifPollTimer = setInterval(tick, 3000);
|
|
2684
|
+
}
|
|
2685
|
+
stopNotifPoll() {
|
|
2686
|
+
if (this.notifPollTimer) {
|
|
2687
|
+
clearInterval(this.notifPollTimer);
|
|
2688
|
+
this.notifPollTimer = undefined;
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
startHubHeartbeat() {
|
|
2692
|
+
this.stopHubHeartbeat();
|
|
2693
|
+
const sendHeartbeat = async () => {
|
|
2694
|
+
try {
|
|
2695
|
+
const hub = this.resolveHubConnection();
|
|
2696
|
+
if (!hub) {
|
|
2697
|
+
const persisted = this.store.getClientHubConnection();
|
|
2698
|
+
if (persisted?.hubUrl && persisted?.userToken) {
|
|
2699
|
+
await (0, hub_1.hubRequestJson)(persisted.hubUrl, persisted.userToken, "/api/v1/hub/heartbeat", { method: "POST" });
|
|
2700
|
+
}
|
|
2701
|
+
return;
|
|
2702
|
+
}
|
|
2703
|
+
await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/heartbeat", { method: "POST" });
|
|
2704
|
+
}
|
|
2705
|
+
catch { /* best-effort */ }
|
|
2706
|
+
};
|
|
2707
|
+
sendHeartbeat();
|
|
2708
|
+
this.hubHeartbeatTimer = setInterval(sendHeartbeat, ViewerServer.HUB_HEARTBEAT_INTERVAL_MS);
|
|
2709
|
+
}
|
|
2710
|
+
stopHubHeartbeat() {
|
|
2711
|
+
if (this.hubHeartbeatTimer) {
|
|
2712
|
+
clearInterval(this.hubHeartbeatTimer);
|
|
2713
|
+
this.hubHeartbeatTimer = undefined;
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2068
2716
|
getLocalIPs() {
|
|
2069
2717
|
const nets = node_os_1.default.networkInterfaces();
|
|
2070
2718
|
const ips = [];
|
|
@@ -2116,7 +2764,7 @@ class ViewerServer {
|
|
|
2116
2764
|
}
|
|
2117
2765
|
}
|
|
2118
2766
|
handleSaveConfig(req, res) {
|
|
2119
|
-
this.readBody(req, (body) => {
|
|
2767
|
+
this.readBody(req, async (body) => {
|
|
2120
2768
|
try {
|
|
2121
2769
|
const newCfg = JSON.parse(body);
|
|
2122
2770
|
const cfgPath = this.getOpenClawConfigPath();
|
|
@@ -2141,6 +2789,10 @@ class ViewerServer {
|
|
|
2141
2789
|
if (!entry.config)
|
|
2142
2790
|
entry.config = {};
|
|
2143
2791
|
const config = entry.config;
|
|
2792
|
+
const oldSharing = config.sharing;
|
|
2793
|
+
const oldSharingRole = oldSharing?.role;
|
|
2794
|
+
const oldSharingEnabled = Boolean(oldSharing?.enabled);
|
|
2795
|
+
const oldClientHubAddress = String(oldSharing?.client?.hubAddress || "");
|
|
2144
2796
|
if (newCfg.embedding)
|
|
2145
2797
|
config.embedding = newCfg.embedding;
|
|
2146
2798
|
if (newCfg.summarizer)
|
|
@@ -2165,7 +2817,8 @@ class ViewerServer {
|
|
|
2165
2817
|
localIPs.push("127.0.0.1", "localhost", "0.0.0.0");
|
|
2166
2818
|
try {
|
|
2167
2819
|
const u = new URL(addr.startsWith("http") ? addr : `http://${addr}`);
|
|
2168
|
-
|
|
2820
|
+
const targetPort = u.port || (u.protocol === "https:" ? "443" : "80");
|
|
2821
|
+
if (localIPs.includes(u.hostname) && targetPort === String(this.port)) {
|
|
2169
2822
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2170
2823
|
res.end(JSON.stringify({ error: "cannot_join_self" }));
|
|
2171
2824
|
return;
|
|
@@ -2174,6 +2827,38 @@ class ViewerServer {
|
|
|
2174
2827
|
catch { }
|
|
2175
2828
|
}
|
|
2176
2829
|
}
|
|
2830
|
+
const newRole = merged.role;
|
|
2831
|
+
const newEnabled = Boolean(merged.enabled);
|
|
2832
|
+
// Detect disabling sharing or switching away from hub mode
|
|
2833
|
+
const wasHub = oldSharingEnabled && oldSharingRole === "hub";
|
|
2834
|
+
const isHub = newEnabled && newRole === "hub";
|
|
2835
|
+
if (wasHub && !isHub) {
|
|
2836
|
+
await this.notifyHubShutdown();
|
|
2837
|
+
this.stopHubHeartbeat();
|
|
2838
|
+
this.log.info("Hub shutting down: notified connected clients");
|
|
2839
|
+
}
|
|
2840
|
+
// Detect disabling sharing or switching away from client mode
|
|
2841
|
+
const wasClient = oldSharingEnabled && oldSharingRole === "client";
|
|
2842
|
+
const isClient = newEnabled && newRole === "client";
|
|
2843
|
+
if (wasClient && !isClient) {
|
|
2844
|
+
this.notifyHubLeave();
|
|
2845
|
+
const oldConn = this.store.getClientHubConnection();
|
|
2846
|
+
if (oldConn) {
|
|
2847
|
+
this.store.setClientHubConnection({ ...oldConn, userToken: "", lastKnownStatus: "left" });
|
|
2848
|
+
}
|
|
2849
|
+
this.log.info("Client hub connection token cleared (sharing disabled or role changed), identity preserved");
|
|
2850
|
+
}
|
|
2851
|
+
if (wasClient && isClient) {
|
|
2852
|
+
const newClientAddr = String(merged.client?.hubAddress || "");
|
|
2853
|
+
if (newClientAddr && oldClientHubAddress && (0, hub_1.normalizeHubUrl)(newClientAddr) !== (0, hub_1.normalizeHubUrl)(oldClientHubAddress)) {
|
|
2854
|
+
this.notifyHubLeave();
|
|
2855
|
+
const oldConn = this.store.getClientHubConnection();
|
|
2856
|
+
if (oldConn) {
|
|
2857
|
+
this.store.setClientHubConnection({ ...oldConn, hubUrl: (0, hub_1.normalizeHubUrl)(newClientAddr), userToken: "", lastKnownStatus: "hub_changed" });
|
|
2858
|
+
}
|
|
2859
|
+
this.log.info("Client hub connection token cleared (switched to different Hub), identity preserved");
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2177
2862
|
if (merged.role === "hub") {
|
|
2178
2863
|
merged.client = { hubAddress: "", userToken: "", teamToken: "" };
|
|
2179
2864
|
}
|
|
@@ -2185,6 +2870,14 @@ class ViewerServer {
|
|
|
2185
2870
|
node_fs_1.default.mkdirSync(node_path_1.default.dirname(cfgPath), { recursive: true });
|
|
2186
2871
|
node_fs_1.default.writeFileSync(cfgPath, JSON.stringify(raw, null, 2), "utf-8");
|
|
2187
2872
|
this.log.info("Plugin config updated via Viewer");
|
|
2873
|
+
this.stopHubHeartbeat();
|
|
2874
|
+
// When switching to client mode or re-enabling sharing as client, send join request
|
|
2875
|
+
const finalSharing = config.sharing;
|
|
2876
|
+
const nowClient = Boolean(finalSharing?.enabled) && finalSharing?.role === "client";
|
|
2877
|
+
const previouslyClient = oldSharingEnabled && oldSharingRole === "client";
|
|
2878
|
+
if (nowClient && !previouslyClient) {
|
|
2879
|
+
this.autoJoinOnSave(finalSharing).catch(e => this.log.warn(`Auto-join on save failed: ${e}`));
|
|
2880
|
+
}
|
|
2188
2881
|
this.jsonResponse(res, { ok: true });
|
|
2189
2882
|
}
|
|
2190
2883
|
catch (e) {
|
|
@@ -2194,6 +2887,97 @@ class ViewerServer {
|
|
|
2194
2887
|
}
|
|
2195
2888
|
});
|
|
2196
2889
|
}
|
|
2890
|
+
async autoJoinOnSave(sharing) {
|
|
2891
|
+
const clientCfg = sharing.client;
|
|
2892
|
+
const hubAddress = String(clientCfg?.hubAddress || "");
|
|
2893
|
+
const teamToken = String(clientCfg?.teamToken || "");
|
|
2894
|
+
if (!hubAddress || !teamToken)
|
|
2895
|
+
return;
|
|
2896
|
+
const hubUrl = (0, hub_1.normalizeHubUrl)(hubAddress);
|
|
2897
|
+
const os = await Promise.resolve().then(() => __importStar(require("os")));
|
|
2898
|
+
const nickname = String(clientCfg?.nickname || "");
|
|
2899
|
+
const username = nickname || os.userInfo().username || "user";
|
|
2900
|
+
const hostname = os.hostname() || "unknown";
|
|
2901
|
+
const persisted = this.store.getClientHubConnection();
|
|
2902
|
+
const existingIdentityKey = persisted?.identityKey || "";
|
|
2903
|
+
const result = await (0, hub_1.hubRequestJson)(hubUrl, "", "/api/v1/hub/join", {
|
|
2904
|
+
method: "POST",
|
|
2905
|
+
body: JSON.stringify({ teamToken, username, deviceName: hostname, identityKey: existingIdentityKey }),
|
|
2906
|
+
});
|
|
2907
|
+
const returnedIdentityKey = String(result.identityKey || existingIdentityKey || "");
|
|
2908
|
+
this.store.setClientHubConnection({
|
|
2909
|
+
hubUrl,
|
|
2910
|
+
userId: String(result.userId || ""),
|
|
2911
|
+
username,
|
|
2912
|
+
userToken: result.userToken || "",
|
|
2913
|
+
role: "member",
|
|
2914
|
+
connectedAt: Date.now(),
|
|
2915
|
+
identityKey: returnedIdentityKey,
|
|
2916
|
+
lastKnownStatus: result.status || "",
|
|
2917
|
+
});
|
|
2918
|
+
this.log.info(`Auto-join on save: status=${result.status}, userId=${result.userId}`);
|
|
2919
|
+
if (result.userToken) {
|
|
2920
|
+
this.startHubHeartbeat();
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
async notifyHubLeave() {
|
|
2924
|
+
try {
|
|
2925
|
+
const hub = this.resolveHubConnection();
|
|
2926
|
+
if (hub) {
|
|
2927
|
+
await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/leave", { method: "POST" });
|
|
2928
|
+
this.log.info("Notified Hub of voluntary leave");
|
|
2929
|
+
return;
|
|
2930
|
+
}
|
|
2931
|
+
const persisted = this.store.getClientHubConnection();
|
|
2932
|
+
if (persisted?.hubUrl && persisted?.userToken) {
|
|
2933
|
+
await (0, hub_1.hubRequestJson)(persisted.hubUrl, persisted.userToken, "/api/v1/hub/leave", { method: "POST" });
|
|
2934
|
+
this.log.info("Notified Hub of voluntary leave (persisted connection)");
|
|
2935
|
+
}
|
|
2936
|
+
}
|
|
2937
|
+
catch (e) {
|
|
2938
|
+
this.log.warn(`Failed to notify Hub of leave: ${e}`);
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
async notifyHubShutdown() {
|
|
2942
|
+
try {
|
|
2943
|
+
const sharing = this.ctx?.config.sharing;
|
|
2944
|
+
if (!sharing || sharing.role !== "hub")
|
|
2945
|
+
return;
|
|
2946
|
+
const hubPort = sharing.hub?.port ?? 18800;
|
|
2947
|
+
const authPath = node_path_1.default.join(this.dataDir, "hub-auth.json");
|
|
2948
|
+
let adminToken;
|
|
2949
|
+
try {
|
|
2950
|
+
const authData = JSON.parse(node_fs_1.default.readFileSync(authPath, "utf8"));
|
|
2951
|
+
adminToken = authData?.bootstrapAdminToken;
|
|
2952
|
+
}
|
|
2953
|
+
catch {
|
|
2954
|
+
return;
|
|
2955
|
+
}
|
|
2956
|
+
if (!adminToken)
|
|
2957
|
+
return;
|
|
2958
|
+
const users = this.store.listHubUsers("active");
|
|
2959
|
+
const { v4: uuidv4 } = require("uuid");
|
|
2960
|
+
for (const u of users) {
|
|
2961
|
+
try {
|
|
2962
|
+
this.store.insertHubNotification({
|
|
2963
|
+
id: uuidv4(),
|
|
2964
|
+
userId: u.id,
|
|
2965
|
+
type: "hub_shutdown",
|
|
2966
|
+
resource: "hub",
|
|
2967
|
+
title: "Hub is shutting down",
|
|
2968
|
+
message: "The Hub server is shutting down. You may be disconnected.",
|
|
2969
|
+
});
|
|
2970
|
+
}
|
|
2971
|
+
catch (e) {
|
|
2972
|
+
this.log.warn(`Failed to insert shutdown notification for user ${u.id}: ${e}`);
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
this.log.info(`Hub shutdown: notified ${users.length} approved user(s)`);
|
|
2976
|
+
}
|
|
2977
|
+
catch (e) {
|
|
2978
|
+
this.log.warn(`notifyHubShutdown error: ${e}`);
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2197
2981
|
handleUpdateUsername(req, res) {
|
|
2198
2982
|
this.readBody(req, async (body) => {
|
|
2199
2983
|
if (!this.ctx)
|
|
@@ -2224,10 +3008,10 @@ class ViewerServer {
|
|
|
2224
3008
|
}
|
|
2225
3009
|
}
|
|
2226
3010
|
else {
|
|
2227
|
-
const
|
|
2228
|
-
if (
|
|
3011
|
+
const persistedConn = this.store.getClientHubConnection();
|
|
3012
|
+
if (persistedConn) {
|
|
2229
3013
|
this.store.setClientHubConnection({
|
|
2230
|
-
...
|
|
3014
|
+
...persistedConn,
|
|
2231
3015
|
username: result.username,
|
|
2232
3016
|
userToken: result.userToken,
|
|
2233
3017
|
});
|
|
@@ -2257,7 +3041,8 @@ class ViewerServer {
|
|
|
2257
3041
|
const localIPs = this.getLocalIPs();
|
|
2258
3042
|
localIPs.push("127.0.0.1", "localhost", "0.0.0.0");
|
|
2259
3043
|
const parsed = new URL(hubUrl.startsWith("http") ? hubUrl : `http://${hubUrl}`);
|
|
2260
|
-
|
|
3044
|
+
const targetPort = parsed.port || (parsed.protocol === "https:" ? "443" : "80");
|
|
3045
|
+
if (localIPs.includes(parsed.hostname) && targetPort === String(this.port)) {
|
|
2261
3046
|
this.jsonResponse(res, { ok: false, error: "cannot_join_self" });
|
|
2262
3047
|
return;
|
|
2263
3048
|
}
|
|
@@ -2668,7 +3453,7 @@ class ViewerServer {
|
|
|
2668
3453
|
// ─── Migration: scan OpenClaw built-in memory ───
|
|
2669
3454
|
getOpenClawHome() {
|
|
2670
3455
|
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
2671
|
-
return node_path_1.default.join(home, ".openclaw");
|
|
3456
|
+
return process.env.OPENCLAW_STATE_DIR || node_path_1.default.join(home, ".openclaw");
|
|
2672
3457
|
}
|
|
2673
3458
|
handleCleanupPolluted(res) {
|
|
2674
3459
|
try {
|
|
@@ -2696,7 +3481,7 @@ class ViewerServer {
|
|
|
2696
3481
|
try {
|
|
2697
3482
|
const ocHome = this.getOpenClawHome();
|
|
2698
3483
|
const memoryDir = node_path_1.default.join(ocHome, "memory");
|
|
2699
|
-
const
|
|
3484
|
+
const agentsDir = node_path_1.default.join(ocHome, "agents");
|
|
2700
3485
|
const sqliteFiles = [];
|
|
2701
3486
|
if (node_fs_1.default.existsSync(memoryDir)) {
|
|
2702
3487
|
for (const f of node_fs_1.default.readdirSync(memoryDir)) {
|
|
@@ -2714,38 +3499,45 @@ class ViewerServer {
|
|
|
2714
3499
|
}
|
|
2715
3500
|
let sessionCount = 0;
|
|
2716
3501
|
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
|
-
|
|
3502
|
+
if (node_fs_1.default.existsSync(agentsDir)) {
|
|
3503
|
+
for (const entry of node_fs_1.default.readdirSync(agentsDir, { withFileTypes: true })) {
|
|
3504
|
+
if (!entry.isDirectory())
|
|
3505
|
+
continue;
|
|
3506
|
+
const sessDir = node_path_1.default.join(agentsDir, entry.name, "sessions");
|
|
3507
|
+
if (!node_fs_1.default.existsSync(sessDir))
|
|
3508
|
+
continue;
|
|
3509
|
+
const jsonlFiles = node_fs_1.default.readdirSync(sessDir).filter(f => f.includes(".jsonl"));
|
|
3510
|
+
sessionCount += jsonlFiles.length;
|
|
3511
|
+
for (const f of jsonlFiles) {
|
|
3512
|
+
try {
|
|
3513
|
+
const content = node_fs_1.default.readFileSync(node_path_1.default.join(sessDir, f), "utf-8");
|
|
3514
|
+
const lines = content.split("\n").filter(l => l.trim());
|
|
3515
|
+
for (const line of lines) {
|
|
3516
|
+
try {
|
|
3517
|
+
const obj = JSON.parse(line);
|
|
3518
|
+
if (obj.type === "message") {
|
|
3519
|
+
const role = obj.message?.role ?? obj.role;
|
|
3520
|
+
if (role === "user" || role === "assistant") {
|
|
3521
|
+
const mc = obj.message?.content ?? obj.content;
|
|
3522
|
+
let txt = "";
|
|
3523
|
+
if (typeof mc === "string")
|
|
3524
|
+
txt = mc;
|
|
3525
|
+
else if (Array.isArray(mc))
|
|
3526
|
+
txt = mc.filter((p) => p.type === "text" && p.text).map((p) => p.text).join("\n");
|
|
3527
|
+
else
|
|
3528
|
+
txt = JSON.stringify(mc);
|
|
3529
|
+
if (role === "user")
|
|
3530
|
+
txt = (0, capture_1.stripInboundMetadata)(txt);
|
|
3531
|
+
if (txt && txt.length >= 10)
|
|
3532
|
+
messageCount++;
|
|
3533
|
+
}
|
|
2742
3534
|
}
|
|
2743
3535
|
}
|
|
3536
|
+
catch { /* skip bad lines */ }
|
|
2744
3537
|
}
|
|
2745
|
-
catch { /* skip bad lines */ }
|
|
2746
3538
|
}
|
|
3539
|
+
catch { /* skip unreadable */ }
|
|
2747
3540
|
}
|
|
2748
|
-
catch { /* skip unreadable */ }
|
|
2749
3541
|
}
|
|
2750
3542
|
}
|
|
2751
3543
|
const cfgPath = this.getOpenClawConfigPath();
|