@memtensor/memos-local-openclaw-plugin 1.0.4-beta.0 → 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 +5 -2
- package/dist/client/connector.d.ts.map +1 -1
- package/dist/client/connector.js +173 -14
- 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 +9 -11
- 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 +301 -106
- package/dist/hub/server.js.map +1 -1
- package/dist/hub/user-manager.d.ts +3 -0
- package/dist/hub/user-manager.d.ts.map +1 -1
- package/dist/hub/user-manager.js +18 -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 +301 -207
- 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 +2991 -1041
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +32 -8
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +1122 -261
- 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 +173 -16
- package/src/client/hub.ts +18 -0
- package/src/client/skill-sync.ts +14 -0
- package/src/config.ts +9 -11
- package/src/hub/server.ts +285 -98
- package/src/hub/user-manager.ts +20 -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 +310 -233
- 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 +2991 -1041
- package/src/viewer/server.ts +984 -190
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,12 @@ 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);
|
|
314
|
+
else if (p === "/api/sharing/retry-join" && req.method === "POST")
|
|
315
|
+
this.handleRetryJoin(req, res);
|
|
285
316
|
else if (p === "/api/sharing/search/memories" && req.method === "POST")
|
|
286
317
|
this.handleSharingMemorySearch(req, res);
|
|
287
318
|
else if (p === "/api/sharing/memories/list" && req.method === "GET")
|
|
@@ -300,6 +331,8 @@ class ViewerServer {
|
|
|
300
331
|
this.handleSharingTaskUnshare(req, res);
|
|
301
332
|
else if (p === "/api/sharing/update-username" && req.method === "POST")
|
|
302
333
|
this.handleUpdateUsername(req, res);
|
|
334
|
+
else if (p === "/api/sharing/rename-user" && req.method === "POST")
|
|
335
|
+
this.handleAdminRenameUser(req, res);
|
|
303
336
|
else if (p === "/api/sharing/test-hub" && req.method === "POST")
|
|
304
337
|
this.handleTestHubConnection(req, res);
|
|
305
338
|
else if (p === "/api/sharing/memories/share" && req.method === "POST")
|
|
@@ -312,28 +345,26 @@ class ViewerServer {
|
|
|
312
345
|
this.handleSharingSkillShare(req, res);
|
|
313
346
|
else if (p === "/api/sharing/skills/unshare" && req.method === "POST")
|
|
314
347
|
this.handleSharingSkillUnshare(req, res);
|
|
315
|
-
else if (p === "/api/sharing/groups" && req.method === "GET")
|
|
316
|
-
this.serveSharingGroups(res);
|
|
317
|
-
else if (p === "/api/sharing/groups" && req.method === "POST")
|
|
318
|
-
this.handleSharingGroupCreate(req, res);
|
|
319
|
-
else if (p.match(/^\/api\/sharing\/groups\/[^/]+$/) && req.method === "PUT")
|
|
320
|
-
this.handleSharingGroupUpdate(req, res, p);
|
|
321
|
-
else if (p.match(/^\/api\/sharing\/groups\/[^/]+$/) && req.method === "DELETE")
|
|
322
|
-
this.handleSharingGroupDelete(res, p);
|
|
323
|
-
else if (p.match(/^\/api\/sharing\/groups\/[^/]+\/members$/) && req.method === "GET")
|
|
324
|
-
this.serveSharingGroupMembers(res, p);
|
|
325
|
-
else if (p.match(/^\/api\/sharing\/groups\/[^/]+\/members$/) && req.method === "POST")
|
|
326
|
-
this.handleSharingGroupAddMember(req, res, p);
|
|
327
|
-
else if (p.match(/^\/api\/sharing\/groups\/[^/]+\/members$/) && req.method === "DELETE")
|
|
328
|
-
this.handleSharingGroupRemoveMember(req, res, p);
|
|
329
348
|
else if (p === "/api/sharing/users" && req.method === "GET")
|
|
330
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);
|
|
331
358
|
else if (p === "/api/admin/shared-tasks" && req.method === "GET")
|
|
332
359
|
this.serveAdminSharedTasks(res);
|
|
360
|
+
else if (p.match(/^\/api\/admin\/shared-tasks\/[^/]+\/detail$/) && req.method === "GET")
|
|
361
|
+
this.serveHubTaskDetail(res, p);
|
|
333
362
|
else if (p.match(/^\/api\/admin\/shared-tasks\/[^/]+$/) && req.method === "DELETE")
|
|
334
363
|
this.handleAdminDeleteTask(res, p);
|
|
335
364
|
else if (p === "/api/admin/shared-skills" && req.method === "GET")
|
|
336
365
|
this.serveAdminSharedSkills(res);
|
|
366
|
+
else if (p.match(/^\/api\/admin\/shared-skills\/[^/]+\/detail$/) && req.method === "GET")
|
|
367
|
+
this.serveHubSkillDetail(res, p);
|
|
337
368
|
else if (p.match(/^\/api\/admin\/shared-skills\/[^/]+$/) && req.method === "DELETE")
|
|
338
369
|
this.handleAdminDeleteSkill(res, p);
|
|
339
370
|
else if (p === "/api/admin/shared-memories" && req.method === "GET")
|
|
@@ -511,7 +542,11 @@ class ViewerServer {
|
|
|
511
542
|
conditions.push("role = ?");
|
|
512
543
|
params.push(role);
|
|
513
544
|
}
|
|
514
|
-
if (owner) {
|
|
545
|
+
if (owner && owner.startsWith("agent:")) {
|
|
546
|
+
conditions.push("(owner = ? OR owner = 'public')");
|
|
547
|
+
params.push(owner);
|
|
548
|
+
}
|
|
549
|
+
else if (owner) {
|
|
515
550
|
conditions.push("owner = ?");
|
|
516
551
|
params.push(owner);
|
|
517
552
|
}
|
|
@@ -525,16 +560,20 @@ class ViewerServer {
|
|
|
525
560
|
}
|
|
526
561
|
const where = conditions.length > 0 ? " WHERE " + conditions.join(" AND ") : "";
|
|
527
562
|
const totalRow = db.prepare("SELECT COUNT(*) as count FROM chunks" + where).get(...params);
|
|
528
|
-
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);
|
|
529
564
|
const findMergeSources = db.prepare("SELECT id, summary, role FROM chunks WHERE dedup_target = ? AND (dedup_status = 'merged' OR dedup_status = 'duplicate')");
|
|
530
565
|
const chunkIds = rawMemories.map((m) => m.id);
|
|
531
566
|
const sharingMap = new Map();
|
|
567
|
+
const localShareMap = new Map();
|
|
532
568
|
if (chunkIds.length > 0) {
|
|
533
569
|
try {
|
|
534
570
|
const placeholders = chunkIds.map(() => "?").join(",");
|
|
535
571
|
const sharedRows = db.prepare(`SELECT source_chunk_id, visibility, group_id FROM hub_memories WHERE source_chunk_id IN (${placeholders})`).all(...chunkIds);
|
|
536
572
|
for (const r of sharedRows)
|
|
537
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);
|
|
538
577
|
}
|
|
539
578
|
catch {
|
|
540
579
|
}
|
|
@@ -546,8 +585,12 @@ class ViewerServer {
|
|
|
546
585
|
out.merge_sources = sources;
|
|
547
586
|
}
|
|
548
587
|
const shared = sharingMap.get(m.id);
|
|
588
|
+
const localShared = localShareMap.get(m.id);
|
|
549
589
|
out.sharingVisibility = shared?.visibility ?? null;
|
|
550
590
|
out.sharingGroupId = shared?.group_id ?? null;
|
|
591
|
+
out.localSharing = out.owner === "public";
|
|
592
|
+
out.localSharingManaged = !!localShared;
|
|
593
|
+
out.localOriginalOwner = localShared?.original_owner ?? null;
|
|
551
594
|
return out;
|
|
552
595
|
});
|
|
553
596
|
this.store.recordViewerEvent("list");
|
|
@@ -562,19 +605,35 @@ class ViewerServer {
|
|
|
562
605
|
this.jsonResponse(res, data);
|
|
563
606
|
}
|
|
564
607
|
serveToolMetrics(res, url) {
|
|
565
|
-
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));
|
|
566
623
|
const data = this.store.getToolMetrics(minutes);
|
|
567
624
|
this.jsonResponse(res, data);
|
|
568
625
|
}
|
|
569
626
|
serveTasks(res, url) {
|
|
570
627
|
this.store.recordViewerEvent("tasks_list");
|
|
571
628
|
const status = url.searchParams.get("status") ?? undefined;
|
|
629
|
+
const owner = url.searchParams.get("owner") ?? undefined;
|
|
572
630
|
const limit = Math.min(100, Math.max(1, Number(url.searchParams.get("limit")) || 50));
|
|
573
631
|
const offset = Math.max(0, Number(url.searchParams.get("offset")) || 0);
|
|
574
|
-
const { tasks, total } = this.store.listTasks({ status, limit, offset });
|
|
632
|
+
const { tasks, total } = this.store.listTasks({ status, limit, offset, owner });
|
|
575
633
|
const db = this.store.db;
|
|
576
634
|
const items = tasks.map((t) => {
|
|
577
|
-
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);
|
|
578
637
|
return {
|
|
579
638
|
id: t.id,
|
|
580
639
|
sessionKey: t.sessionKey,
|
|
@@ -585,6 +644,8 @@ class ViewerServer {
|
|
|
585
644
|
endedAt: t.endedAt,
|
|
586
645
|
chunkCount: this.store.countChunksByTask(t.id),
|
|
587
646
|
skillStatus: meta?.skill_status ?? null,
|
|
647
|
+
owner: meta?.owner ?? "agent:main",
|
|
648
|
+
sharingVisibility: sharedTask?.visibility ?? null,
|
|
588
649
|
};
|
|
589
650
|
});
|
|
590
651
|
this.jsonResponse(res, { tasks: items, total, limit, offset });
|
|
@@ -620,6 +681,7 @@ class ViewerServer {
|
|
|
620
681
|
title: task.title,
|
|
621
682
|
summary: task.summary,
|
|
622
683
|
status: task.status,
|
|
684
|
+
owner: task.owner ?? "agent:main",
|
|
623
685
|
startedAt: task.startedAt,
|
|
624
686
|
endedAt: task.endedAt,
|
|
625
687
|
chunks: chunkItems,
|
|
@@ -628,6 +690,7 @@ class ViewerServer {
|
|
|
628
690
|
skillLinks,
|
|
629
691
|
sharingVisibility: sharedTask?.visibility ?? null,
|
|
630
692
|
sharingGroupId: sharedTask?.group_id ?? null,
|
|
693
|
+
hubTaskId: sharedTask ? true : false,
|
|
631
694
|
});
|
|
632
695
|
}
|
|
633
696
|
serveStats(res, url) {
|
|
@@ -661,12 +724,21 @@ class ViewerServer {
|
|
|
661
724
|
embCount = db.prepare("SELECT COUNT(*) as count FROM embeddings").get().count;
|
|
662
725
|
}
|
|
663
726
|
catch { /* table may not exist */ }
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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);
|
|
670
742
|
let skillCount = 0;
|
|
671
743
|
try {
|
|
672
744
|
skillCount = db.prepare("SELECT COUNT(*) as count FROM skills").get().count;
|
|
@@ -680,10 +752,17 @@ class ViewerServer {
|
|
|
680
752
|
catch { /* column may not exist yet */ }
|
|
681
753
|
let owners = [];
|
|
682
754
|
try {
|
|
683
|
-
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();
|
|
684
756
|
owners = ownerRows.map((o) => o.owner);
|
|
685
757
|
}
|
|
686
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 */ }
|
|
687
766
|
this.jsonResponse(res, {
|
|
688
767
|
totalMemories: total.count, totalSessions: sessions.count, totalEmbeddings: embCount,
|
|
689
768
|
totalSkills: skillCount,
|
|
@@ -692,6 +771,7 @@ class ViewerServer {
|
|
|
692
771
|
timeRange: { earliest: timeRange.earliest, latest: timeRange.latest },
|
|
693
772
|
sessions: sessionList,
|
|
694
773
|
owners,
|
|
774
|
+
currentAgentOwner,
|
|
695
775
|
});
|
|
696
776
|
}
|
|
697
777
|
catch (e) {
|
|
@@ -815,7 +895,12 @@ class ViewerServer {
|
|
|
815
895
|
if (visibility) {
|
|
816
896
|
skills = skills.filter(s => s.visibility === visibility);
|
|
817
897
|
}
|
|
818
|
-
|
|
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 });
|
|
819
904
|
}
|
|
820
905
|
serveSkillDetail(res, urlPath) {
|
|
821
906
|
const skillId = urlPath.replace("/api/skill/", "");
|
|
@@ -1075,7 +1160,7 @@ class ViewerServer {
|
|
|
1075
1160
|
}
|
|
1076
1161
|
});
|
|
1077
1162
|
}
|
|
1078
|
-
handleSkillDelete(res, urlPath) {
|
|
1163
|
+
async handleSkillDelete(res, urlPath) {
|
|
1079
1164
|
const skillId = urlPath.replace("/api/skill/", "");
|
|
1080
1165
|
const skill = this.store.getSkill(skillId);
|
|
1081
1166
|
if (!skill) {
|
|
@@ -1083,7 +1168,18 @@ class ViewerServer {
|
|
|
1083
1168
|
res.end(JSON.stringify({ error: "Skill not found" }));
|
|
1084
1169
|
return;
|
|
1085
1170
|
}
|
|
1086
|
-
|
|
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 (_) { }
|
|
1087
1183
|
try {
|
|
1088
1184
|
if (skill.dirPath && node_fs_1.default.existsSync(skill.dirPath)) {
|
|
1089
1185
|
node_fs_1.default.rmSync(skill.dirPath, { recursive: true, force: true });
|
|
@@ -1133,7 +1229,15 @@ class ViewerServer {
|
|
|
1133
1229
|
const cleaned = chunk.role === "user" && chunk.content
|
|
1134
1230
|
? { ...chunk, content: (0, capture_1.stripInboundMetadata)(chunk.content) }
|
|
1135
1231
|
: chunk;
|
|
1136
|
-
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
|
+
});
|
|
1137
1241
|
}
|
|
1138
1242
|
handleUpdate(req, res, urlPath) {
|
|
1139
1243
|
const chunkId = urlPath.replace("/api/memory/", "");
|
|
@@ -1168,6 +1272,360 @@ class ViewerServer {
|
|
|
1168
1272
|
res.end(JSON.stringify({ error: "Not found" }));
|
|
1169
1273
|
}
|
|
1170
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
|
+
}
|
|
1171
1629
|
handleDeleteSession(res, url) {
|
|
1172
1630
|
const key = url.searchParams.get("key");
|
|
1173
1631
|
if (!key) {
|
|
@@ -1205,7 +1663,8 @@ class ViewerServer {
|
|
|
1205
1663
|
// ─── Config API ───
|
|
1206
1664
|
getOpenClawConfigPath() {
|
|
1207
1665
|
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
1208
|
-
|
|
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");
|
|
1209
1668
|
}
|
|
1210
1669
|
getPluginEntryConfig(raw) {
|
|
1211
1670
|
const entries = raw?.plugins?.entries ?? {};
|
|
@@ -1261,8 +1720,7 @@ class ViewerServer {
|
|
|
1261
1720
|
base.admin.rejectSupported = true;
|
|
1262
1721
|
base.connection.connected = true;
|
|
1263
1722
|
base.connection.hubUrl = resolvedHubUrl ?? undefined;
|
|
1264
|
-
|
|
1265
|
-
let adminUser = { username: "hub-admin", role: "admin", groups: [] };
|
|
1723
|
+
let adminUser = { username: "hub-admin", role: "admin" };
|
|
1266
1724
|
try {
|
|
1267
1725
|
const hub = this.resolveHubConnection();
|
|
1268
1726
|
if (hub) {
|
|
@@ -1272,7 +1730,6 @@ class ViewerServer {
|
|
|
1272
1730
|
id: me.id,
|
|
1273
1731
|
username: me.username ?? "hub-admin",
|
|
1274
1732
|
role: me.role ?? "admin",
|
|
1275
|
-
groups: Array.isArray(me.groups) ? me.groups : [],
|
|
1276
1733
|
};
|
|
1277
1734
|
}
|
|
1278
1735
|
}
|
|
@@ -1290,13 +1747,23 @@ class ViewerServer {
|
|
|
1290
1747
|
this.jsonResponse(res, base);
|
|
1291
1748
|
return;
|
|
1292
1749
|
}
|
|
1293
|
-
|
|
1750
|
+
const hasPendingConnection = Boolean(persisted?.hubUrl && persisted?.userId && !persisted?.userToken);
|
|
1751
|
+
if (!hasClientConfig && !hasPendingConnection) {
|
|
1294
1752
|
this.jsonResponse(res, base);
|
|
1295
1753
|
return;
|
|
1296
1754
|
}
|
|
1297
1755
|
try {
|
|
1298
1756
|
const status = await (0, connector_1.getHubStatus)(this.store, this.ctx.config);
|
|
1299
1757
|
const output = { ...base, connection: { ...base.connection, ...status } };
|
|
1758
|
+
if (status.user?.status === "pending") {
|
|
1759
|
+
output.connection.pendingApproval = true;
|
|
1760
|
+
}
|
|
1761
|
+
if (status.user?.status === "rejected") {
|
|
1762
|
+
output.connection.rejected = true;
|
|
1763
|
+
}
|
|
1764
|
+
if (status.user?.status === "removed") {
|
|
1765
|
+
output.connection.removed = true;
|
|
1766
|
+
}
|
|
1300
1767
|
if (status.connected && status.hubUrl) {
|
|
1301
1768
|
try {
|
|
1302
1769
|
const info = await fetch(`${status.hubUrl}/api/v1/hub/info`).then((r) => (r.ok ? r.json() : null)).catch(() => null);
|
|
@@ -1305,6 +1772,13 @@ class ViewerServer {
|
|
|
1305
1772
|
}
|
|
1306
1773
|
catch { }
|
|
1307
1774
|
}
|
|
1775
|
+
else if (status.hubUrl) {
|
|
1776
|
+
try {
|
|
1777
|
+
const info = await fetch(`${status.hubUrl}/api/v1/hub/info`).then((r) => (r.ok ? r.json() : null)).catch(() => null);
|
|
1778
|
+
output.connection.teamName = info?.teamName ?? null;
|
|
1779
|
+
}
|
|
1780
|
+
catch { }
|
|
1781
|
+
}
|
|
1308
1782
|
output.admin.canManageUsers = status.connected && status.user?.role === "admin";
|
|
1309
1783
|
output.admin.rejectSupported = output.admin.canManageUsers;
|
|
1310
1784
|
this.jsonResponse(res, output);
|
|
@@ -1367,81 +1841,227 @@ class ViewerServer {
|
|
|
1367
1841
|
}
|
|
1368
1842
|
});
|
|
1369
1843
|
}
|
|
1370
|
-
|
|
1371
|
-
if (!this.ctx)
|
|
1372
|
-
return this.jsonResponse(res, { memories: [], error: "sharing_unavailable" });
|
|
1373
|
-
try {
|
|
1374
|
-
const limit = Number(url.searchParams.get("limit") || 40);
|
|
1375
|
-
const data = await (0, hub_1.hubListMemories)(this.store, this.ctx, { limit });
|
|
1376
|
-
this.jsonResponse(res, { memories: Array.isArray(data?.memories) ? data.memories : [] });
|
|
1377
|
-
}
|
|
1378
|
-
catch (err) {
|
|
1379
|
-
this.jsonResponse(res, { memories: [], error: String(err) });
|
|
1380
|
-
}
|
|
1381
|
-
}
|
|
1382
|
-
async serveSharingTaskList(res, url) {
|
|
1383
|
-
if (!this.ctx)
|
|
1384
|
-
return this.jsonResponse(res, { tasks: [], error: "sharing_unavailable" });
|
|
1385
|
-
try {
|
|
1386
|
-
const limit = Number(url.searchParams.get("limit") || 40);
|
|
1387
|
-
const data = await (0, hub_1.hubListTasks)(this.store, this.ctx, { limit });
|
|
1388
|
-
this.jsonResponse(res, { tasks: Array.isArray(data?.tasks) ? data.tasks : [] });
|
|
1389
|
-
}
|
|
1390
|
-
catch (err) {
|
|
1391
|
-
this.jsonResponse(res, { tasks: [], error: String(err) });
|
|
1392
|
-
}
|
|
1393
|
-
}
|
|
1394
|
-
async serveSharingSkillList(res, url) {
|
|
1395
|
-
if (!this.ctx)
|
|
1396
|
-
return this.jsonResponse(res, { skills: [], error: "sharing_unavailable" });
|
|
1397
|
-
try {
|
|
1398
|
-
const limit = Number(url.searchParams.get("limit") || 40);
|
|
1399
|
-
const data = await (0, hub_1.hubListSkills)(this.store, this.ctx, { limit });
|
|
1400
|
-
this.jsonResponse(res, { skills: Array.isArray(data?.skills) ? data.skills : [] });
|
|
1401
|
-
}
|
|
1402
|
-
catch (err) {
|
|
1403
|
-
this.jsonResponse(res, { skills: [], error: String(err) });
|
|
1404
|
-
}
|
|
1405
|
-
}
|
|
1406
|
-
handleSharingMemorySearch(req, res) {
|
|
1844
|
+
handleSharingChangeRole(req, res) {
|
|
1407
1845
|
this.readBody(req, async (body) => {
|
|
1408
1846
|
if (!this.ctx)
|
|
1409
|
-
return this.jsonResponse(res, {
|
|
1410
|
-
const emptyHub = { hits: [], meta: { totalCandidates: 0, searchedGroups: [], includedPublic: false } };
|
|
1847
|
+
return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
1411
1848
|
try {
|
|
1412
1849
|
const parsed = JSON.parse(body || "{}");
|
|
1413
|
-
const
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
const
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
}
|
|
1421
|
-
try {
|
|
1422
|
-
const hub = await (0, hub_1.hubSearchMemories)(this.store, this.ctx, { query, maxResults, scope });
|
|
1423
|
-
this.jsonResponse(res, { local: { hits: local.hits, meta: local.meta }, hub });
|
|
1424
|
-
}
|
|
1425
|
-
catch (err) {
|
|
1426
|
-
this.jsonResponse(res, { local: { hits: local.hits, meta: local.meta }, hub: emptyHub, error: String(err) });
|
|
1427
|
-
}
|
|
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 });
|
|
1428
1858
|
}
|
|
1429
1859
|
catch (err) {
|
|
1430
|
-
this.jsonResponse(res, {
|
|
1860
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1431
1861
|
}
|
|
1432
1862
|
});
|
|
1433
1863
|
}
|
|
1434
|
-
|
|
1864
|
+
handleSharingRemoveUser(req, res) {
|
|
1435
1865
|
this.readBody(req, async (body) => {
|
|
1436
1866
|
if (!this.ctx)
|
|
1437
|
-
return this.jsonResponse(res, { error: "sharing_unavailable" });
|
|
1867
|
+
return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
1438
1868
|
try {
|
|
1439
1869
|
const parsed = JSON.parse(body || "{}");
|
|
1440
|
-
const
|
|
1441
|
-
|
|
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 });
|
|
1442
1878
|
}
|
|
1443
1879
|
catch (err) {
|
|
1444
|
-
this.jsonResponse(res, { error: String(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
|
+
}
|
|
1913
|
+
handleRetryJoin(req, res) {
|
|
1914
|
+
this.readBody(req, async (_body) => {
|
|
1915
|
+
if (!this.ctx)
|
|
1916
|
+
return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
1917
|
+
const sharing = this.ctx.config.sharing;
|
|
1918
|
+
if (!sharing?.enabled || sharing.role !== "client") {
|
|
1919
|
+
return this.jsonResponse(res, { ok: false, error: "not_in_client_mode" });
|
|
1920
|
+
}
|
|
1921
|
+
const hubAddress = sharing.client?.hubAddress ?? "";
|
|
1922
|
+
const teamToken = sharing.client?.teamToken ?? "";
|
|
1923
|
+
if (!hubAddress || !teamToken) {
|
|
1924
|
+
return this.jsonResponse(res, { ok: false, error: "missing_hub_address_or_team_token" });
|
|
1925
|
+
}
|
|
1926
|
+
try {
|
|
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 { }
|
|
1937
|
+
const os = await Promise.resolve().then(() => __importStar(require("os")));
|
|
1938
|
+
const nickname = sharing.client?.nickname;
|
|
1939
|
+
const username = nickname || os.userInfo().username || "user";
|
|
1940
|
+
const hostname = os.hostname() || "unknown";
|
|
1941
|
+
const result = await (0, hub_1.hubRequestJson)(hubUrl, "", "/api/v1/hub/join", {
|
|
1942
|
+
method: "POST",
|
|
1943
|
+
body: JSON.stringify({ teamToken, username, deviceName: hostname, reapply: true }),
|
|
1944
|
+
});
|
|
1945
|
+
this.store.setClientHubConnection({
|
|
1946
|
+
hubUrl,
|
|
1947
|
+
userId: String(result.userId || ""),
|
|
1948
|
+
username,
|
|
1949
|
+
userToken: result.userToken || "",
|
|
1950
|
+
role: "member",
|
|
1951
|
+
connectedAt: Date.now(),
|
|
1952
|
+
});
|
|
1953
|
+
this.jsonResponse(res, { ok: true, status: result.status || "pending" });
|
|
1954
|
+
}
|
|
1955
|
+
catch (err) {
|
|
1956
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1957
|
+
}
|
|
1958
|
+
});
|
|
1959
|
+
}
|
|
1960
|
+
async serveSharingMemoryList(res, url) {
|
|
1961
|
+
if (!this.ctx)
|
|
1962
|
+
return this.jsonResponse(res, { memories: [], error: "sharing_unavailable" });
|
|
1963
|
+
try {
|
|
1964
|
+
const limit = Number(url.searchParams.get("limit") || 40);
|
|
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
|
+
}
|
|
1973
|
+
this.jsonResponse(res, { memories: Array.isArray(data?.memories) ? data.memories : [] });
|
|
1974
|
+
}
|
|
1975
|
+
catch (err) {
|
|
1976
|
+
this.jsonResponse(res, { memories: [], error: String(err) });
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
async serveSharingTaskList(res, url) {
|
|
1980
|
+
if (!this.ctx)
|
|
1981
|
+
return this.jsonResponse(res, { tasks: [], error: "sharing_unavailable" });
|
|
1982
|
+
try {
|
|
1983
|
+
const limit = Number(url.searchParams.get("limit") || 40);
|
|
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
|
+
}
|
|
1992
|
+
this.jsonResponse(res, { tasks: Array.isArray(data?.tasks) ? data.tasks : [] });
|
|
1993
|
+
}
|
|
1994
|
+
catch (err) {
|
|
1995
|
+
this.jsonResponse(res, { tasks: [], error: String(err) });
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
async serveSharingSkillList(res, url) {
|
|
1999
|
+
if (!this.ctx)
|
|
2000
|
+
return this.jsonResponse(res, { skills: [], error: "sharing_unavailable" });
|
|
2001
|
+
try {
|
|
2002
|
+
const limit = Number(url.searchParams.get("limit") || 40);
|
|
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
|
+
}
|
|
2011
|
+
this.jsonResponse(res, { skills: Array.isArray(data?.skills) ? data.skills : [] });
|
|
2012
|
+
}
|
|
2013
|
+
catch (err) {
|
|
2014
|
+
this.jsonResponse(res, { skills: [], error: String(err) });
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
handleSharingMemorySearch(req, res) {
|
|
2018
|
+
this.readBody(req, async (body) => {
|
|
2019
|
+
if (!this.ctx)
|
|
2020
|
+
return this.jsonResponse(res, { local: { hits: [], meta: {} }, hub: { hits: [], meta: { totalCandidates: 0, searchedGroups: [], includedPublic: false } }, error: "sharing_unavailable" });
|
|
2021
|
+
const emptyHub = { hits: [], meta: { totalCandidates: 0, searchedGroups: [], includedPublic: false } };
|
|
2022
|
+
try {
|
|
2023
|
+
const parsed = JSON.parse(body || "{}");
|
|
2024
|
+
const query = String(parsed.query || "");
|
|
2025
|
+
const role = typeof parsed.role === "string" ? parsed.role : undefined;
|
|
2026
|
+
const maxResults = typeof parsed.maxResults === "number" ? parsed.maxResults : 10;
|
|
2027
|
+
const scope = parsed.scope === "group" || parsed.scope === "all" || parsed.scope === "hub" ? (parsed.scope === "hub" ? "all" : parsed.scope) : "local";
|
|
2028
|
+
const local = this.searchLocalViewerMemories(query, { role, maxResults });
|
|
2029
|
+
if (scope === "local") {
|
|
2030
|
+
return this.jsonResponse(res, { local: { hits: local.hits, meta: local.meta }, hub: emptyHub });
|
|
2031
|
+
}
|
|
2032
|
+
try {
|
|
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
|
+
}
|
|
2043
|
+
this.jsonResponse(res, { local: { hits: local.hits, meta: local.meta }, hub });
|
|
2044
|
+
}
|
|
2045
|
+
catch (err) {
|
|
2046
|
+
this.jsonResponse(res, { local: { hits: local.hits, meta: local.meta }, hub: emptyHub, error: String(err) });
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
catch (err) {
|
|
2050
|
+
this.jsonResponse(res, { local: { hits: [], meta: {} }, hub: emptyHub, error: String(err) });
|
|
2051
|
+
}
|
|
2052
|
+
});
|
|
2053
|
+
}
|
|
2054
|
+
handleSharingMemoryDetail(req, res) {
|
|
2055
|
+
this.readBody(req, async (body) => {
|
|
2056
|
+
if (!this.ctx)
|
|
2057
|
+
return this.jsonResponse(res, { error: "sharing_unavailable" });
|
|
2058
|
+
try {
|
|
2059
|
+
const parsed = JSON.parse(body || "{}");
|
|
2060
|
+
const detail = await (0, hub_1.hubGetMemoryDetail)(this.store, this.ctx, { remoteHitId: String(parsed.remoteHitId || "") });
|
|
2061
|
+
this.jsonResponse(res, detail);
|
|
2062
|
+
}
|
|
2063
|
+
catch (err) {
|
|
2064
|
+
this.jsonResponse(res, { error: String(err) });
|
|
1445
2065
|
}
|
|
1446
2066
|
});
|
|
1447
2067
|
}
|
|
@@ -1515,8 +2135,8 @@ class ViewerServer {
|
|
|
1515
2135
|
try {
|
|
1516
2136
|
const parsed = JSON.parse(body || "{}");
|
|
1517
2137
|
const taskId = String(parsed.taskId || "");
|
|
1518
|
-
const visibility =
|
|
1519
|
-
const groupId =
|
|
2138
|
+
const visibility = "public";
|
|
2139
|
+
const groupId = undefined;
|
|
1520
2140
|
const task = this.store.getTask(taskId);
|
|
1521
2141
|
if (!task)
|
|
1522
2142
|
return this.jsonResponse(res, { ok: false, error: "task_not_found" });
|
|
@@ -1532,7 +2152,7 @@ class ViewerServer {
|
|
|
1532
2152
|
sourceTaskId: task.id,
|
|
1533
2153
|
title: task.title,
|
|
1534
2154
|
summary: task.summary,
|
|
1535
|
-
groupId:
|
|
2155
|
+
groupId: null,
|
|
1536
2156
|
visibility,
|
|
1537
2157
|
createdAt: task.startedAt ?? Date.now(),
|
|
1538
2158
|
updatedAt: task.updatedAt ?? Date.now(),
|
|
@@ -1558,7 +2178,7 @@ class ViewerServer {
|
|
|
1558
2178
|
sourceUserId: hubUserId,
|
|
1559
2179
|
title: task.title,
|
|
1560
2180
|
summary: task.summary,
|
|
1561
|
-
groupId:
|
|
2181
|
+
groupId: null,
|
|
1562
2182
|
visibility,
|
|
1563
2183
|
createdAt: task.startedAt ?? Date.now(),
|
|
1564
2184
|
updatedAt: task.updatedAt ?? Date.now(),
|
|
@@ -1603,8 +2223,8 @@ class ViewerServer {
|
|
|
1603
2223
|
try {
|
|
1604
2224
|
const parsed = JSON.parse(body || "{}");
|
|
1605
2225
|
const chunkId = String(parsed.chunkId || "");
|
|
1606
|
-
const visibility =
|
|
1607
|
-
const groupId =
|
|
2226
|
+
const visibility = "public";
|
|
2227
|
+
const groupId = undefined;
|
|
1608
2228
|
const db = this.store.db;
|
|
1609
2229
|
const chunk = db.prepare("SELECT * FROM chunks WHERE id = ?").get(chunkId);
|
|
1610
2230
|
if (!chunk)
|
|
@@ -1619,7 +2239,7 @@ class ViewerServer {
|
|
|
1619
2239
|
content: chunk.content,
|
|
1620
2240
|
summary: chunk.summary,
|
|
1621
2241
|
kind: chunk.kind,
|
|
1622
|
-
groupId:
|
|
2242
|
+
groupId: null,
|
|
1623
2243
|
visibility,
|
|
1624
2244
|
},
|
|
1625
2245
|
}),
|
|
@@ -1636,7 +2256,7 @@ class ViewerServer {
|
|
|
1636
2256
|
content: chunk.content,
|
|
1637
2257
|
summary: chunk.summary ?? "",
|
|
1638
2258
|
kind: chunk.kind,
|
|
1639
|
-
groupId:
|
|
2259
|
+
groupId: null,
|
|
1640
2260
|
visibility,
|
|
1641
2261
|
createdAt: existing?.createdAt ?? now,
|
|
1642
2262
|
updatedAt: now,
|
|
@@ -1694,8 +2314,8 @@ class ViewerServer {
|
|
|
1694
2314
|
try {
|
|
1695
2315
|
const parsed = JSON.parse(body || "{}");
|
|
1696
2316
|
const skillId = String(parsed.skillId || "");
|
|
1697
|
-
const visibility =
|
|
1698
|
-
const groupId =
|
|
2317
|
+
const visibility = "public";
|
|
2318
|
+
const groupId = null;
|
|
1699
2319
|
const skill = this.store.getSkill(skillId);
|
|
1700
2320
|
if (!skill)
|
|
1701
2321
|
return this.jsonResponse(res, { ok: false, error: "skill_not_found" });
|
|
@@ -1705,7 +2325,7 @@ class ViewerServer {
|
|
|
1705
2325
|
method: "POST",
|
|
1706
2326
|
body: JSON.stringify({
|
|
1707
2327
|
visibility,
|
|
1708
|
-
groupId:
|
|
2328
|
+
groupId: null,
|
|
1709
2329
|
metadata: bundle.metadata,
|
|
1710
2330
|
bundle: bundle.bundle,
|
|
1711
2331
|
}),
|
|
@@ -1720,7 +2340,7 @@ class ViewerServer {
|
|
|
1720
2340
|
name: skill.name,
|
|
1721
2341
|
description: skill.description,
|
|
1722
2342
|
version: skill.version,
|
|
1723
|
-
groupId:
|
|
2343
|
+
groupId: null,
|
|
1724
2344
|
visibility,
|
|
1725
2345
|
bundle: JSON.stringify(bundle.bundle),
|
|
1726
2346
|
qualityScore: skill.qualityScore,
|
|
@@ -1807,123 +2427,6 @@ class ViewerServer {
|
|
|
1807
2427
|
}
|
|
1808
2428
|
return (0, hub_1.resolveHubClient)(this.store, this.ctx);
|
|
1809
2429
|
}
|
|
1810
|
-
extractGroupId(path) {
|
|
1811
|
-
const m = path.match(/\/api\/sharing\/groups\/([^/]+)/);
|
|
1812
|
-
return m ? decodeURIComponent(m[1]) : "";
|
|
1813
|
-
}
|
|
1814
|
-
async serveSharingGroups(res) {
|
|
1815
|
-
const hub = this.resolveHubConnection();
|
|
1816
|
-
if (!hub)
|
|
1817
|
-
return this.jsonResponse(res, { groups: [], error: "not_configured" });
|
|
1818
|
-
try {
|
|
1819
|
-
const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/groups", { method: "GET" });
|
|
1820
|
-
this.jsonResponse(res, { groups: Array.isArray(data?.groups) ? data.groups : [] });
|
|
1821
|
-
}
|
|
1822
|
-
catch (err) {
|
|
1823
|
-
this.jsonResponse(res, { groups: [], error: String(err) });
|
|
1824
|
-
}
|
|
1825
|
-
}
|
|
1826
|
-
handleSharingGroupCreate(req, res) {
|
|
1827
|
-
this.readBody(req, async (body) => {
|
|
1828
|
-
const hub = this.resolveHubConnection();
|
|
1829
|
-
if (!hub)
|
|
1830
|
-
return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1831
|
-
try {
|
|
1832
|
-
const parsed = JSON.parse(body || "{}");
|
|
1833
|
-
const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/groups", {
|
|
1834
|
-
method: "POST",
|
|
1835
|
-
body: JSON.stringify({ name: parsed.name, description: parsed.description }),
|
|
1836
|
-
});
|
|
1837
|
-
this.jsonResponse(res, { ok: true, ...data });
|
|
1838
|
-
}
|
|
1839
|
-
catch (err) {
|
|
1840
|
-
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1841
|
-
}
|
|
1842
|
-
});
|
|
1843
|
-
}
|
|
1844
|
-
handleSharingGroupUpdate(req, res, p) {
|
|
1845
|
-
this.readBody(req, async (body) => {
|
|
1846
|
-
const hub = this.resolveHubConnection();
|
|
1847
|
-
if (!hub)
|
|
1848
|
-
return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1849
|
-
const groupId = this.extractGroupId(p);
|
|
1850
|
-
try {
|
|
1851
|
-
const parsed = JSON.parse(body || "{}");
|
|
1852
|
-
await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}`, {
|
|
1853
|
-
method: "PUT",
|
|
1854
|
-
body: JSON.stringify({ name: parsed.name, description: parsed.description }),
|
|
1855
|
-
});
|
|
1856
|
-
this.jsonResponse(res, { ok: true });
|
|
1857
|
-
}
|
|
1858
|
-
catch (err) {
|
|
1859
|
-
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1860
|
-
}
|
|
1861
|
-
});
|
|
1862
|
-
}
|
|
1863
|
-
async handleSharingGroupDelete(res, p) {
|
|
1864
|
-
const hub = this.resolveHubConnection();
|
|
1865
|
-
if (!hub)
|
|
1866
|
-
return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1867
|
-
const groupId = this.extractGroupId(p);
|
|
1868
|
-
try {
|
|
1869
|
-
await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}`, { method: "DELETE" });
|
|
1870
|
-
this.jsonResponse(res, { ok: true });
|
|
1871
|
-
}
|
|
1872
|
-
catch (err) {
|
|
1873
|
-
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1874
|
-
}
|
|
1875
|
-
}
|
|
1876
|
-
async serveSharingGroupMembers(res, p) {
|
|
1877
|
-
const hub = this.resolveHubConnection();
|
|
1878
|
-
if (!hub)
|
|
1879
|
-
return this.jsonResponse(res, { members: [], error: "not_configured" });
|
|
1880
|
-
const groupId = this.extractGroupId(p);
|
|
1881
|
-
try {
|
|
1882
|
-
const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}`, { method: "GET" });
|
|
1883
|
-
this.jsonResponse(res, { members: Array.isArray(data?.members) ? data.members : [] });
|
|
1884
|
-
}
|
|
1885
|
-
catch (err) {
|
|
1886
|
-
this.jsonResponse(res, { members: [], error: String(err) });
|
|
1887
|
-
}
|
|
1888
|
-
}
|
|
1889
|
-
handleSharingGroupAddMember(req, res, p) {
|
|
1890
|
-
this.readBody(req, async (body) => {
|
|
1891
|
-
const hub = this.resolveHubConnection();
|
|
1892
|
-
if (!hub)
|
|
1893
|
-
return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1894
|
-
const groupId = this.extractGroupId(p);
|
|
1895
|
-
try {
|
|
1896
|
-
const parsed = JSON.parse(body || "{}");
|
|
1897
|
-
await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}/members`, {
|
|
1898
|
-
method: "POST",
|
|
1899
|
-
body: JSON.stringify({ userId: parsed.userId }),
|
|
1900
|
-
});
|
|
1901
|
-
this.jsonResponse(res, { ok: true });
|
|
1902
|
-
}
|
|
1903
|
-
catch (err) {
|
|
1904
|
-
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1905
|
-
}
|
|
1906
|
-
});
|
|
1907
|
-
}
|
|
1908
|
-
handleSharingGroupRemoveMember(req, res, p) {
|
|
1909
|
-
this.readBody(req, async (body) => {
|
|
1910
|
-
const hub = this.resolveHubConnection();
|
|
1911
|
-
if (!hub)
|
|
1912
|
-
return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1913
|
-
const groupId = this.extractGroupId(p);
|
|
1914
|
-
try {
|
|
1915
|
-
const parsed = JSON.parse(body || "{}");
|
|
1916
|
-
await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}/members`, {
|
|
1917
|
-
method: "DELETE",
|
|
1918
|
-
body: JSON.stringify({ userId: parsed.userId }),
|
|
1919
|
-
});
|
|
1920
|
-
this.jsonResponse(res, { ok: true });
|
|
1921
|
-
}
|
|
1922
|
-
catch (err) {
|
|
1923
|
-
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1924
|
-
}
|
|
1925
|
-
});
|
|
1926
|
-
}
|
|
1927
2430
|
async serveSharingUsers(res) {
|
|
1928
2431
|
const hub = this.resolveHubConnection();
|
|
1929
2432
|
if (!hub)
|
|
@@ -1943,7 +2446,17 @@ class ViewerServer {
|
|
|
1943
2446
|
return this.jsonResponse(res, { tasks: [], error: "not_configured" });
|
|
1944
2447
|
try {
|
|
1945
2448
|
const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/shared-tasks", { method: "GET" });
|
|
1946
|
-
|
|
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 });
|
|
1947
2460
|
}
|
|
1948
2461
|
catch (err) {
|
|
1949
2462
|
this.jsonResponse(res, { tasks: [], error: String(err) });
|
|
@@ -1962,13 +2475,55 @@ class ViewerServer {
|
|
|
1962
2475
|
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1963
2476
|
}
|
|
1964
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
|
+
}
|
|
1965
2510
|
async serveAdminSharedSkills(res) {
|
|
1966
2511
|
const hub = this.resolveHubConnection();
|
|
1967
2512
|
if (!hub)
|
|
1968
2513
|
return this.jsonResponse(res, { skills: [], error: "not_configured" });
|
|
1969
2514
|
try {
|
|
1970
2515
|
const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/shared-skills", { method: "GET" });
|
|
1971
|
-
|
|
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 });
|
|
1972
2527
|
}
|
|
1973
2528
|
catch (err) {
|
|
1974
2529
|
this.jsonResponse(res, { skills: [], error: String(err) });
|
|
@@ -1993,7 +2548,18 @@ class ViewerServer {
|
|
|
1993
2548
|
return this.jsonResponse(res, { memories: [], error: "not_configured" });
|
|
1994
2549
|
try {
|
|
1995
2550
|
const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/shared-memories", { method: "GET" });
|
|
1996
|
-
|
|
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 });
|
|
1997
2563
|
}
|
|
1998
2564
|
catch (err) {
|
|
1999
2565
|
this.jsonResponse(res, { memories: [], error: String(err) });
|
|
@@ -2012,7 +2578,136 @@ class ViewerServer {
|
|
|
2012
2578
|
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
2013
2579
|
}
|
|
2014
2580
|
}
|
|
2015
|
-
|
|
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
|
+
}
|
|
2710
|
+
getLocalIPs() {
|
|
2016
2711
|
const nets = node_os_1.default.networkInterfaces();
|
|
2017
2712
|
const ips = [];
|
|
2018
2713
|
for (const name of Object.keys(nets)) {
|
|
@@ -2022,6 +2717,10 @@ class ViewerServer {
|
|
|
2022
2717
|
}
|
|
2023
2718
|
}
|
|
2024
2719
|
}
|
|
2720
|
+
return ips;
|
|
2721
|
+
}
|
|
2722
|
+
serveLocalIPs(res) {
|
|
2723
|
+
const ips = this.getLocalIPs();
|
|
2025
2724
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2026
2725
|
res.end(JSON.stringify({ ips }));
|
|
2027
2726
|
}
|
|
@@ -2059,7 +2758,7 @@ class ViewerServer {
|
|
|
2059
2758
|
}
|
|
2060
2759
|
}
|
|
2061
2760
|
handleSaveConfig(req, res) {
|
|
2062
|
-
this.readBody(req, (body) => {
|
|
2761
|
+
this.readBody(req, async (body) => {
|
|
2063
2762
|
try {
|
|
2064
2763
|
const newCfg = JSON.parse(body);
|
|
2065
2764
|
const cfgPath = this.getOpenClawConfigPath();
|
|
@@ -2084,6 +2783,10 @@ class ViewerServer {
|
|
|
2084
2783
|
if (!entry.config)
|
|
2085
2784
|
entry.config = {};
|
|
2086
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 || "");
|
|
2087
2790
|
if (newCfg.embedding)
|
|
2088
2791
|
config.embedding = newCfg.embedding;
|
|
2089
2792
|
if (newCfg.summarizer)
|
|
@@ -2097,15 +2800,70 @@ class ViewerServer {
|
|
|
2097
2800
|
if (newCfg.sharing !== undefined) {
|
|
2098
2801
|
const existing = config.sharing || {};
|
|
2099
2802
|
const merged = { ...existing, ...newCfg.sharing };
|
|
2100
|
-
// Deep-merge capabilities so new keys don't wipe existing ones
|
|
2101
2803
|
if (newCfg.sharing.capabilities && existing.capabilities) {
|
|
2102
2804
|
merged.capabilities = { ...existing.capabilities, ...newCfg.sharing.capabilities };
|
|
2103
2805
|
}
|
|
2806
|
+
if (merged.role === "client" && merged.client) {
|
|
2807
|
+
const clientCfg = merged.client;
|
|
2808
|
+
const addr = String(clientCfg.hubAddress || "");
|
|
2809
|
+
if (addr) {
|
|
2810
|
+
const localIPs = this.getLocalIPs();
|
|
2811
|
+
localIPs.push("127.0.0.1", "localhost", "0.0.0.0");
|
|
2812
|
+
try {
|
|
2813
|
+
const u = new URL(addr.startsWith("http") ? addr : `http://${addr}`);
|
|
2814
|
+
if (localIPs.includes(u.hostname)) {
|
|
2815
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2816
|
+
res.end(JSON.stringify({ error: "cannot_join_self" }));
|
|
2817
|
+
return;
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
catch { }
|
|
2821
|
+
}
|
|
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
|
+
}
|
|
2850
|
+
if (merged.role === "hub") {
|
|
2851
|
+
merged.client = { hubAddress: "", userToken: "", teamToken: "" };
|
|
2852
|
+
}
|
|
2853
|
+
else if (merged.role === "client") {
|
|
2854
|
+
merged.hub = { port: 18800, teamName: "", teamToken: "" };
|
|
2855
|
+
}
|
|
2104
2856
|
config.sharing = merged;
|
|
2105
2857
|
}
|
|
2106
2858
|
node_fs_1.default.mkdirSync(node_path_1.default.dirname(cfgPath), { recursive: true });
|
|
2107
2859
|
node_fs_1.default.writeFileSync(cfgPath, JSON.stringify(raw, null, 2), "utf-8");
|
|
2108
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
|
+
}
|
|
2109
2867
|
this.jsonResponse(res, { ok: true });
|
|
2110
2868
|
}
|
|
2111
2869
|
catch (e) {
|
|
@@ -2115,6 +2873,92 @@ class ViewerServer {
|
|
|
2115
2873
|
}
|
|
2116
2874
|
});
|
|
2117
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
|
+
}
|
|
2118
2962
|
handleUpdateUsername(req, res) {
|
|
2119
2963
|
this.readBody(req, async (body) => {
|
|
2120
2964
|
if (!this.ctx)
|
|
@@ -2174,6 +3018,16 @@ class ViewerServer {
|
|
|
2174
3018
|
this.jsonResponse(res, { ok: false, error: "hubUrl is required" });
|
|
2175
3019
|
return;
|
|
2176
3020
|
}
|
|
3021
|
+
try {
|
|
3022
|
+
const localIPs = this.getLocalIPs();
|
|
3023
|
+
localIPs.push("127.0.0.1", "localhost", "0.0.0.0");
|
|
3024
|
+
const parsed = new URL(hubUrl.startsWith("http") ? hubUrl : `http://${hubUrl}`);
|
|
3025
|
+
if (localIPs.includes(parsed.hostname)) {
|
|
3026
|
+
this.jsonResponse(res, { ok: false, error: "cannot_join_self" });
|
|
3027
|
+
return;
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
catch { }
|
|
2177
3031
|
const url = hubUrl.replace(/\/+$/, "") + "/api/v1/hub/info";
|
|
2178
3032
|
const ctrl = new AbortController();
|
|
2179
3033
|
const timeout = setTimeout(() => ctrl.abort(), 8000);
|
|
@@ -2579,7 +3433,7 @@ class ViewerServer {
|
|
|
2579
3433
|
// ─── Migration: scan OpenClaw built-in memory ───
|
|
2580
3434
|
getOpenClawHome() {
|
|
2581
3435
|
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
2582
|
-
return node_path_1.default.join(home, ".openclaw");
|
|
3436
|
+
return process.env.OPENCLAW_STATE_DIR || node_path_1.default.join(home, ".openclaw");
|
|
2583
3437
|
}
|
|
2584
3438
|
handleCleanupPolluted(res) {
|
|
2585
3439
|
try {
|
|
@@ -2607,7 +3461,7 @@ class ViewerServer {
|
|
|
2607
3461
|
try {
|
|
2608
3462
|
const ocHome = this.getOpenClawHome();
|
|
2609
3463
|
const memoryDir = node_path_1.default.join(ocHome, "memory");
|
|
2610
|
-
const
|
|
3464
|
+
const agentsDir = node_path_1.default.join(ocHome, "agents");
|
|
2611
3465
|
const sqliteFiles = [];
|
|
2612
3466
|
if (node_fs_1.default.existsSync(memoryDir)) {
|
|
2613
3467
|
for (const f of node_fs_1.default.readdirSync(memoryDir)) {
|
|
@@ -2625,38 +3479,45 @@ class ViewerServer {
|
|
|
2625
3479
|
}
|
|
2626
3480
|
let sessionCount = 0;
|
|
2627
3481
|
let messageCount = 0;
|
|
2628
|
-
if (node_fs_1.default.existsSync(
|
|
2629
|
-
const
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
txt =
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
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
|
+
}
|
|
2653
3514
|
}
|
|
2654
3515
|
}
|
|
3516
|
+
catch { /* skip bad lines */ }
|
|
2655
3517
|
}
|
|
2656
|
-
catch { /* skip bad lines */ }
|
|
2657
3518
|
}
|
|
3519
|
+
catch { /* skip unreadable */ }
|
|
2658
3520
|
}
|
|
2659
|
-
catch { /* skip unreadable */ }
|
|
2660
3521
|
}
|
|
2661
3522
|
}
|
|
2662
3523
|
const cfgPath = this.getOpenClawConfigPath();
|