@memtensor/memos-local-openclaw-plugin 1.0.4-beta.2 → 1.0.4-beta.20
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 +111 -44
- package/dist/capture/index.d.ts +1 -1
- package/dist/capture/index.d.ts.map +1 -1
- package/dist/capture/index.js +36 -2
- package/dist/capture/index.js.map +1 -1
- package/dist/client/connector.d.ts +6 -2
- package/dist/client/connector.d.ts.map +1 -1
- package/dist/client/connector.js +160 -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 +2 -3
- package/dist/config.js.map +1 -1
- package/dist/hub/server.d.ts +9 -0
- package/dist/hub/server.d.ts.map +1 -1
- package/dist/hub/server.js +500 -112
- 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/chunker.d.ts +2 -1
- package/dist/ingest/chunker.d.ts.map +1 -1
- package/dist/ingest/chunker.js +14 -10
- package/dist/ingest/chunker.js.map +1 -1
- package/dist/ingest/providers/index.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 +96 -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 +84 -9
- 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 +115 -20
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +458 -110
- 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 +2952 -910
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +39 -8
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +1198 -227
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +774 -74
- package/openclaw.plugin.json +2 -2
- 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 +40 -1
- package/src/client/connector.ts +161 -28
- package/src/client/hub.ts +18 -0
- package/src/client/skill-sync.ts +14 -0
- package/src/config.ts +2 -3
- package/src/hub/server.ts +481 -107
- package/src/hub/user-manager.ts +48 -8
- package/src/index.ts +10 -2
- package/src/ingest/chunker.ts +19 -13
- package/src/ingest/providers/index.ts +41 -7
- package/src/recall/engine.ts +89 -1
- package/src/shared/llm-call.ts +99 -10
- 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 +498 -137
- 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 +2952 -910
- package/src/viewer/server.ts +1109 -212
package/dist/viewer/server.js
CHANGED
|
@@ -72,6 +72,8 @@ class ViewerServer {
|
|
|
72
72
|
authFile;
|
|
73
73
|
auth;
|
|
74
74
|
ctx;
|
|
75
|
+
cookieName;
|
|
76
|
+
defaultHubPort;
|
|
75
77
|
static SESSION_TTL = 24 * 60 * 60 * 1000;
|
|
76
78
|
static PLUGIN_VERSION = (() => {
|
|
77
79
|
try {
|
|
@@ -91,6 +93,11 @@ class ViewerServer {
|
|
|
91
93
|
ppAbort = false;
|
|
92
94
|
ppState = { running: false, done: false, stopped: false, processed: 0, total: 0, tasksCreated: 0, skillsCreated: 0, errors: 0, skippedSessions: 0, totalSessions: 0 };
|
|
93
95
|
ppSSEClients = [];
|
|
96
|
+
notifSSEClients = [];
|
|
97
|
+
notifPollTimer;
|
|
98
|
+
lastKnownNotifCount = 0;
|
|
99
|
+
hubHeartbeatTimer;
|
|
100
|
+
static HUB_HEARTBEAT_INTERVAL_MS = 45_000;
|
|
94
101
|
constructor(opts) {
|
|
95
102
|
this.store = opts.store;
|
|
96
103
|
this.embedder = opts.embedder;
|
|
@@ -100,25 +107,41 @@ class ViewerServer {
|
|
|
100
107
|
this.ctx = opts.ctx;
|
|
101
108
|
this.authFile = node_path_1.default.join(opts.dataDir, "viewer-auth.json");
|
|
102
109
|
this.auth = { passwordHash: null, sessions: new Map() };
|
|
110
|
+
this.cookieName = `memos_token_${opts.port}`;
|
|
111
|
+
this.defaultHubPort = opts.defaultHubPort ?? 18800;
|
|
103
112
|
this.resetToken = node_crypto_1.default.randomBytes(16).toString("hex");
|
|
104
113
|
this.loadAuth();
|
|
105
114
|
}
|
|
115
|
+
getHubPort() {
|
|
116
|
+
const configured = this.ctx?.config?.sharing?.hub?.port;
|
|
117
|
+
if (configured && configured !== 18800)
|
|
118
|
+
return configured;
|
|
119
|
+
return this.defaultHubPort;
|
|
120
|
+
}
|
|
106
121
|
start() {
|
|
122
|
+
const MAX_PORT_RETRIES = 5;
|
|
107
123
|
return new Promise((resolve, reject) => {
|
|
124
|
+
let retries = 0;
|
|
108
125
|
this.server = node_http_1.default.createServer((req, res) => this.handleRequest(req, res));
|
|
109
126
|
this.server.on("error", (err) => {
|
|
110
|
-
if (err.code === "EADDRINUSE") {
|
|
111
|
-
|
|
112
|
-
this.
|
|
127
|
+
if (err.code === "EADDRINUSE" && retries < MAX_PORT_RETRIES) {
|
|
128
|
+
retries++;
|
|
129
|
+
const nextPort = this.port + retries;
|
|
130
|
+
this.log.warn(`Viewer port ${this.port + retries - 1} in use, trying ${nextPort}`);
|
|
131
|
+
this.server.listen(nextPort, "0.0.0.0");
|
|
132
|
+
}
|
|
133
|
+
else if (err.code === "EADDRINUSE") {
|
|
134
|
+
reject(new Error(`Viewer failed to find open port after ${MAX_PORT_RETRIES} retries (tried ${this.port}–${this.port + MAX_PORT_RETRIES})`));
|
|
113
135
|
}
|
|
114
136
|
else {
|
|
115
137
|
reject(err);
|
|
116
138
|
}
|
|
117
139
|
});
|
|
118
|
-
this.server.listen(this.port, "
|
|
140
|
+
this.server.listen(this.port, "0.0.0.0", () => {
|
|
119
141
|
const addr = this.server.address();
|
|
120
142
|
const actualPort = typeof addr === "object" && addr ? addr.port : this.port;
|
|
121
143
|
this.autoCleanupPolluted();
|
|
144
|
+
this.startHubHeartbeat();
|
|
122
145
|
resolve(`http://127.0.0.1:${actualPort}`);
|
|
123
146
|
});
|
|
124
147
|
});
|
|
@@ -141,6 +164,15 @@ class ViewerServer {
|
|
|
141
164
|
}
|
|
142
165
|
}
|
|
143
166
|
stop() {
|
|
167
|
+
this.stopHubHeartbeat();
|
|
168
|
+
this.stopNotifPoll();
|
|
169
|
+
for (const c of this.notifSSEClients) {
|
|
170
|
+
try {
|
|
171
|
+
c.end();
|
|
172
|
+
}
|
|
173
|
+
catch { }
|
|
174
|
+
}
|
|
175
|
+
this.notifSSEClients = [];
|
|
144
176
|
this.server?.close();
|
|
145
177
|
this.server = null;
|
|
146
178
|
}
|
|
@@ -178,7 +210,8 @@ class ViewerServer {
|
|
|
178
210
|
}
|
|
179
211
|
isValidSession(req) {
|
|
180
212
|
const cookie = req.headers.cookie ?? "";
|
|
181
|
-
const
|
|
213
|
+
const re = new RegExp(`${this.cookieName}=([a-f0-9]+)`);
|
|
214
|
+
const match = cookie.match(re);
|
|
182
215
|
if (!match)
|
|
183
216
|
return false;
|
|
184
217
|
const expiry = this.auth.sessions.get(match[1]);
|
|
@@ -228,6 +261,16 @@ class ViewerServer {
|
|
|
228
261
|
}
|
|
229
262
|
if (p === "/api/memories" && req.method === "GET")
|
|
230
263
|
this.serveMemories(res, url);
|
|
264
|
+
else if (p === "/api/memories/share-local" && req.method === "POST")
|
|
265
|
+
this.handleMemoryLocalShare(req, res);
|
|
266
|
+
else if (p === "/api/memories/unshare-local" && req.method === "POST")
|
|
267
|
+
this.handleMemoryLocalUnshare(req, res);
|
|
268
|
+
else if (p.match(/^\/api\/memory\/[^/]+\/scope$/) && req.method === "PUT")
|
|
269
|
+
this.handleMemoryScope(req, res, p);
|
|
270
|
+
else if (p.match(/^\/api\/task\/[^/]+\/scope$/) && req.method === "PUT")
|
|
271
|
+
this.handleTaskScope(req, res, p);
|
|
272
|
+
else if (p.match(/^\/api\/skill\/[^/]+\/scope$/) && req.method === "PUT")
|
|
273
|
+
this.handleSkillScope(req, res, p);
|
|
231
274
|
else if (p === "/api/stats")
|
|
232
275
|
this.serveStats(res, url);
|
|
233
276
|
else if (p === "/api/metrics")
|
|
@@ -282,8 +325,14 @@ class ViewerServer {
|
|
|
282
325
|
this.handleSharingApproveUser(req, res);
|
|
283
326
|
else if (p === "/api/sharing/reject-user" && req.method === "POST")
|
|
284
327
|
this.handleSharingRejectUser(req, res);
|
|
328
|
+
else if (p === "/api/sharing/remove-user" && req.method === "POST")
|
|
329
|
+
this.handleSharingRemoveUser(req, res);
|
|
330
|
+
else if (p === "/api/sharing/change-role" && req.method === "POST")
|
|
331
|
+
this.handleSharingChangeRole(req, res);
|
|
285
332
|
else if (p === "/api/sharing/retry-join" && req.method === "POST")
|
|
286
333
|
this.handleRetryJoin(req, res);
|
|
334
|
+
else if (p === "/api/sharing/leave" && req.method === "POST")
|
|
335
|
+
this.handleLeaveTeam(req, res);
|
|
287
336
|
else if (p === "/api/sharing/search/memories" && req.method === "POST")
|
|
288
337
|
this.handleSharingMemorySearch(req, res);
|
|
289
338
|
else if (p === "/api/sharing/memories/list" && req.method === "GET")
|
|
@@ -302,6 +351,8 @@ class ViewerServer {
|
|
|
302
351
|
this.handleSharingTaskUnshare(req, res);
|
|
303
352
|
else if (p === "/api/sharing/update-username" && req.method === "POST")
|
|
304
353
|
this.handleUpdateUsername(req, res);
|
|
354
|
+
else if (p === "/api/sharing/rename-user" && req.method === "POST")
|
|
355
|
+
this.handleAdminRenameUser(req, res);
|
|
305
356
|
else if (p === "/api/sharing/test-hub" && req.method === "POST")
|
|
306
357
|
this.handleTestHubConnection(req, res);
|
|
307
358
|
else if (p === "/api/sharing/memories/share" && req.method === "POST")
|
|
@@ -314,28 +365,28 @@ class ViewerServer {
|
|
|
314
365
|
this.handleSharingSkillShare(req, res);
|
|
315
366
|
else if (p === "/api/sharing/skills/unshare" && req.method === "POST")
|
|
316
367
|
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
368
|
else if (p === "/api/sharing/users" && req.method === "GET")
|
|
332
369
|
this.serveSharingUsers(res);
|
|
370
|
+
else if (p === "/api/sharing/notifications" && req.method === "GET")
|
|
371
|
+
this.serveSharingNotifications(res, url);
|
|
372
|
+
else if (p === "/api/sharing/notifications/read" && req.method === "POST")
|
|
373
|
+
this.handleSharingNotificationsRead(req, res);
|
|
374
|
+
else if (p === "/api/sharing/notifications/clear" && req.method === "POST")
|
|
375
|
+
this.handleSharingNotificationsClear(req, res);
|
|
376
|
+
else if (p === "/api/sharing/sync-hub-removal" && req.method === "POST")
|
|
377
|
+
this.handleSyncHubRemoval(req, res);
|
|
378
|
+
else if (p === "/api/notifications/stream" && req.method === "GET")
|
|
379
|
+
this.handleNotifSSE(req, res);
|
|
333
380
|
else if (p === "/api/admin/shared-tasks" && req.method === "GET")
|
|
334
381
|
this.serveAdminSharedTasks(res);
|
|
382
|
+
else if (p.match(/^\/api\/admin\/shared-tasks\/[^/]+\/detail$/) && req.method === "GET")
|
|
383
|
+
this.serveHubTaskDetail(res, p);
|
|
335
384
|
else if (p.match(/^\/api\/admin\/shared-tasks\/[^/]+$/) && req.method === "DELETE")
|
|
336
385
|
this.handleAdminDeleteTask(res, p);
|
|
337
386
|
else if (p === "/api/admin/shared-skills" && req.method === "GET")
|
|
338
387
|
this.serveAdminSharedSkills(res);
|
|
388
|
+
else if (p.match(/^\/api\/admin\/shared-skills\/[^/]+\/detail$/) && req.method === "GET")
|
|
389
|
+
this.serveHubSkillDetail(res, p);
|
|
339
390
|
else if (p.match(/^\/api\/admin\/shared-skills\/[^/]+$/) && req.method === "DELETE")
|
|
340
391
|
this.handleAdminDeleteSkill(res, p);
|
|
341
392
|
else if (p === "/api/admin/shared-memories" && req.method === "GET")
|
|
@@ -411,7 +462,7 @@ class ViewerServer {
|
|
|
411
462
|
const token = this.createSession();
|
|
412
463
|
res.writeHead(200, {
|
|
413
464
|
"Content-Type": "application/json",
|
|
414
|
-
"Set-Cookie":
|
|
465
|
+
"Set-Cookie": `${this.cookieName}=${token}; Path=/; HttpOnly; SameSite=Strict; Max-Age=86400`,
|
|
415
466
|
});
|
|
416
467
|
res.end(JSON.stringify({ ok: true, message: "Password set successfully" }));
|
|
417
468
|
}
|
|
@@ -433,7 +484,7 @@ class ViewerServer {
|
|
|
433
484
|
const token = this.createSession();
|
|
434
485
|
res.writeHead(200, {
|
|
435
486
|
"Content-Type": "application/json",
|
|
436
|
-
"Set-Cookie":
|
|
487
|
+
"Set-Cookie": `${this.cookieName}=${token}; Path=/; HttpOnly; SameSite=Strict; Max-Age=86400`,
|
|
437
488
|
});
|
|
438
489
|
res.end(JSON.stringify({ ok: true }));
|
|
439
490
|
}
|
|
@@ -445,12 +496,13 @@ class ViewerServer {
|
|
|
445
496
|
}
|
|
446
497
|
handleLogout(req, res) {
|
|
447
498
|
const cookie = req.headers.cookie ?? "";
|
|
448
|
-
const
|
|
499
|
+
const re = new RegExp(`${this.cookieName}=([a-f0-9]+)`);
|
|
500
|
+
const match = cookie.match(re);
|
|
449
501
|
if (match)
|
|
450
502
|
this.auth.sessions.delete(match[1]);
|
|
451
503
|
res.writeHead(200, {
|
|
452
504
|
"Content-Type": "application/json",
|
|
453
|
-
"Set-Cookie":
|
|
505
|
+
"Set-Cookie": `${this.cookieName}=; Path=/; HttpOnly; Max-Age=0`,
|
|
454
506
|
});
|
|
455
507
|
res.end(JSON.stringify({ ok: true }));
|
|
456
508
|
}
|
|
@@ -476,7 +528,7 @@ class ViewerServer {
|
|
|
476
528
|
const sessionToken = this.createSession();
|
|
477
529
|
res.writeHead(200, {
|
|
478
530
|
"Content-Type": "application/json",
|
|
479
|
-
"Set-Cookie":
|
|
531
|
+
"Set-Cookie": `${this.cookieName}=${sessionToken}; Path=/; HttpOnly; SameSite=Strict; Max-Age=86400`,
|
|
480
532
|
});
|
|
481
533
|
res.end(JSON.stringify({ ok: true, message: "Password reset successfully" }));
|
|
482
534
|
}
|
|
@@ -513,7 +565,11 @@ class ViewerServer {
|
|
|
513
565
|
conditions.push("role = ?");
|
|
514
566
|
params.push(role);
|
|
515
567
|
}
|
|
516
|
-
if (owner) {
|
|
568
|
+
if (owner && owner.startsWith("agent:")) {
|
|
569
|
+
conditions.push("(owner = ? OR owner = 'public')");
|
|
570
|
+
params.push(owner);
|
|
571
|
+
}
|
|
572
|
+
else if (owner) {
|
|
517
573
|
conditions.push("owner = ?");
|
|
518
574
|
params.push(owner);
|
|
519
575
|
}
|
|
@@ -527,16 +583,26 @@ class ViewerServer {
|
|
|
527
583
|
}
|
|
528
584
|
const where = conditions.length > 0 ? " WHERE " + conditions.join(" AND ") : "";
|
|
529
585
|
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);
|
|
586
|
+
const rawMemories = db.prepare("SELECT * FROM chunks" + where + ` ORDER BY CASE WHEN dedup_status IN ('duplicate','merged') THEN 1 ELSE 0 END ASC, created_at ${sortBy} LIMIT ? OFFSET ?`).all(...params, limit, offset);
|
|
531
587
|
const findMergeSources = db.prepare("SELECT id, summary, role FROM chunks WHERE dedup_target = ? AND (dedup_status = 'merged' OR dedup_status = 'duplicate')");
|
|
532
588
|
const chunkIds = rawMemories.map((m) => m.id);
|
|
533
589
|
const sharingMap = new Map();
|
|
590
|
+
const localShareMap = new Map();
|
|
534
591
|
if (chunkIds.length > 0) {
|
|
535
592
|
try {
|
|
536
593
|
const placeholders = chunkIds.map(() => "?").join(",");
|
|
537
594
|
const sharedRows = db.prepare(`SELECT source_chunk_id, visibility, group_id FROM hub_memories WHERE source_chunk_id IN (${placeholders})`).all(...chunkIds);
|
|
538
595
|
for (const r of sharedRows)
|
|
539
596
|
sharingMap.set(r.source_chunk_id, r);
|
|
597
|
+
const teamMetaRows = db.prepare(`SELECT chunk_id, visibility, group_id FROM team_shared_chunks WHERE chunk_id IN (${placeholders})`).all(...chunkIds);
|
|
598
|
+
for (const r of teamMetaRows) {
|
|
599
|
+
if (!sharingMap.has(r.chunk_id)) {
|
|
600
|
+
sharingMap.set(r.chunk_id, { visibility: r.visibility, group_id: r.group_id });
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
const localRows = db.prepare(`SELECT chunk_id, original_owner, shared_at FROM local_shared_memories WHERE chunk_id IN (${placeholders})`).all(...chunkIds);
|
|
604
|
+
for (const r of localRows)
|
|
605
|
+
localShareMap.set(r.chunk_id, r);
|
|
540
606
|
}
|
|
541
607
|
catch {
|
|
542
608
|
}
|
|
@@ -548,8 +614,12 @@ class ViewerServer {
|
|
|
548
614
|
out.merge_sources = sources;
|
|
549
615
|
}
|
|
550
616
|
const shared = sharingMap.get(m.id);
|
|
617
|
+
const localShared = localShareMap.get(m.id);
|
|
551
618
|
out.sharingVisibility = shared?.visibility ?? null;
|
|
552
619
|
out.sharingGroupId = shared?.group_id ?? null;
|
|
620
|
+
out.localSharing = out.owner === "public";
|
|
621
|
+
out.localSharingManaged = !!localShared;
|
|
622
|
+
out.localOriginalOwner = localShared?.original_owner ?? null;
|
|
553
623
|
return out;
|
|
554
624
|
});
|
|
555
625
|
this.store.recordViewerEvent("list");
|
|
@@ -564,19 +634,35 @@ class ViewerServer {
|
|
|
564
634
|
this.jsonResponse(res, data);
|
|
565
635
|
}
|
|
566
636
|
serveToolMetrics(res, url) {
|
|
567
|
-
const
|
|
637
|
+
const fromParam = url.searchParams.get("from");
|
|
638
|
+
const toParam = url.searchParams.get("to");
|
|
639
|
+
if (fromParam) {
|
|
640
|
+
const fromMs = new Date(fromParam).getTime();
|
|
641
|
+
const toMs = toParam ? new Date(toParam).getTime() : Date.now();
|
|
642
|
+
if (isNaN(fromMs) || isNaN(toMs)) {
|
|
643
|
+
this.jsonResponse(res, { error: "Invalid date" }, 400);
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
const diffMin = Math.max(10, Math.min(43200, Math.round((toMs - fromMs) / 60000)));
|
|
647
|
+
const data = this.store.getToolMetrics(diffMin, fromMs, toMs);
|
|
648
|
+
this.jsonResponse(res, data);
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
const minutes = Math.min(43200, Math.max(10, Number(url.searchParams.get("minutes")) || 60));
|
|
568
652
|
const data = this.store.getToolMetrics(minutes);
|
|
569
653
|
this.jsonResponse(res, data);
|
|
570
654
|
}
|
|
571
655
|
serveTasks(res, url) {
|
|
572
656
|
this.store.recordViewerEvent("tasks_list");
|
|
573
657
|
const status = url.searchParams.get("status") ?? undefined;
|
|
658
|
+
const owner = url.searchParams.get("owner") ?? undefined;
|
|
574
659
|
const limit = Math.min(100, Math.max(1, Number(url.searchParams.get("limit")) || 50));
|
|
575
660
|
const offset = Math.max(0, Number(url.searchParams.get("offset")) || 0);
|
|
576
|
-
const { tasks, total } = this.store.listTasks({ status, limit, offset });
|
|
661
|
+
const { tasks, total } = this.store.listTasks({ status, limit, offset, owner });
|
|
577
662
|
const db = this.store.db;
|
|
578
663
|
const items = tasks.map((t) => {
|
|
579
|
-
const meta = db.prepare("SELECT skill_status FROM tasks WHERE id = ?").get(t.id);
|
|
664
|
+
const meta = db.prepare("SELECT skill_status, owner FROM tasks WHERE id = ?").get(t.id);
|
|
665
|
+
const sharedTask = db.prepare("SELECT visibility FROM hub_tasks WHERE source_task_id = ? ORDER BY updated_at DESC LIMIT 1").get(t.id);
|
|
580
666
|
return {
|
|
581
667
|
id: t.id,
|
|
582
668
|
sessionKey: t.sessionKey,
|
|
@@ -587,6 +673,8 @@ class ViewerServer {
|
|
|
587
673
|
endedAt: t.endedAt,
|
|
588
674
|
chunkCount: this.store.countChunksByTask(t.id),
|
|
589
675
|
skillStatus: meta?.skill_status ?? null,
|
|
676
|
+
owner: meta?.owner ?? "agent:main",
|
|
677
|
+
sharingVisibility: sharedTask?.visibility ?? null,
|
|
590
678
|
};
|
|
591
679
|
});
|
|
592
680
|
this.jsonResponse(res, { tasks: items, total, limit, offset });
|
|
@@ -622,6 +710,7 @@ class ViewerServer {
|
|
|
622
710
|
title: task.title,
|
|
623
711
|
summary: task.summary,
|
|
624
712
|
status: task.status,
|
|
713
|
+
owner: task.owner ?? "agent:main",
|
|
625
714
|
startedAt: task.startedAt,
|
|
626
715
|
endedAt: task.endedAt,
|
|
627
716
|
chunks: chunkItems,
|
|
@@ -630,6 +719,7 @@ class ViewerServer {
|
|
|
630
719
|
skillLinks,
|
|
631
720
|
sharingVisibility: sharedTask?.visibility ?? null,
|
|
632
721
|
sharingGroupId: sharedTask?.group_id ?? null,
|
|
722
|
+
hubTaskId: sharedTask ? true : false,
|
|
633
723
|
});
|
|
634
724
|
}
|
|
635
725
|
serveStats(res, url) {
|
|
@@ -663,12 +753,21 @@ class ViewerServer {
|
|
|
663
753
|
embCount = db.prepare("SELECT COUNT(*) as count FROM embeddings").get().count;
|
|
664
754
|
}
|
|
665
755
|
catch { /* table may not exist */ }
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
756
|
+
let sessionQuery;
|
|
757
|
+
let sessionParams;
|
|
758
|
+
if (ownerFilter && ownerFilter.startsWith("agent:")) {
|
|
759
|
+
sessionQuery = "SELECT session_key, COUNT(*) as count, MIN(created_at) as earliest, MAX(created_at) as latest FROM chunks WHERE (owner = ? OR owner = 'public') GROUP BY session_key ORDER BY latest DESC";
|
|
760
|
+
sessionParams = [ownerFilter];
|
|
761
|
+
}
|
|
762
|
+
else if (ownerFilter) {
|
|
763
|
+
sessionQuery = "SELECT session_key, COUNT(*) as count, MIN(created_at) as earliest, MAX(created_at) as latest FROM chunks WHERE owner = ? GROUP BY session_key ORDER BY latest DESC";
|
|
764
|
+
sessionParams = [ownerFilter];
|
|
765
|
+
}
|
|
766
|
+
else {
|
|
767
|
+
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";
|
|
768
|
+
sessionParams = [];
|
|
769
|
+
}
|
|
770
|
+
const sessionList = db.prepare(sessionQuery).all(...sessionParams);
|
|
672
771
|
let skillCount = 0;
|
|
673
772
|
try {
|
|
674
773
|
skillCount = db.prepare("SELECT COUNT(*) as count FROM skills").get().count;
|
|
@@ -682,10 +781,17 @@ class ViewerServer {
|
|
|
682
781
|
catch { /* column may not exist yet */ }
|
|
683
782
|
let owners = [];
|
|
684
783
|
try {
|
|
685
|
-
const ownerRows = db.prepare("SELECT DISTINCT owner FROM chunks WHERE owner IS NOT NULL ORDER BY owner").all();
|
|
784
|
+
const ownerRows = db.prepare("SELECT DISTINCT owner FROM chunks WHERE owner IS NOT NULL AND owner LIKE 'agent:%' ORDER BY owner").all();
|
|
686
785
|
owners = ownerRows.map((o) => o.owner);
|
|
687
786
|
}
|
|
688
787
|
catch { /* column may not exist yet */ }
|
|
788
|
+
let currentAgentOwner = "agent:main";
|
|
789
|
+
try {
|
|
790
|
+
const latest = db.prepare("SELECT owner FROM chunks WHERE owner IS NOT NULL AND owner LIKE 'agent:%' ORDER BY created_at DESC LIMIT 1").get();
|
|
791
|
+
if (latest?.owner)
|
|
792
|
+
currentAgentOwner = latest.owner;
|
|
793
|
+
}
|
|
794
|
+
catch { /* best-effort */ }
|
|
689
795
|
this.jsonResponse(res, {
|
|
690
796
|
totalMemories: total.count, totalSessions: sessions.count, totalEmbeddings: embCount,
|
|
691
797
|
totalSkills: skillCount,
|
|
@@ -694,6 +800,7 @@ class ViewerServer {
|
|
|
694
800
|
timeRange: { earliest: timeRange.earliest, latest: timeRange.latest },
|
|
695
801
|
sessions: sessionList,
|
|
696
802
|
owners,
|
|
803
|
+
currentAgentOwner,
|
|
697
804
|
});
|
|
698
805
|
}
|
|
699
806
|
catch (e) {
|
|
@@ -817,7 +924,12 @@ class ViewerServer {
|
|
|
817
924
|
if (visibility) {
|
|
818
925
|
skills = skills.filter(s => s.visibility === visibility);
|
|
819
926
|
}
|
|
820
|
-
|
|
927
|
+
const db = this.store.db;
|
|
928
|
+
const enriched = skills.map(s => {
|
|
929
|
+
const hub = db.prepare("SELECT visibility FROM hub_skills WHERE source_skill_id = ? ORDER BY updated_at DESC LIMIT 1").get(s.id);
|
|
930
|
+
return { ...s, sharingVisibility: hub?.visibility ?? null };
|
|
931
|
+
});
|
|
932
|
+
this.jsonResponse(res, { skills: enriched });
|
|
821
933
|
}
|
|
822
934
|
serveSkillDetail(res, urlPath) {
|
|
823
935
|
const skillId = urlPath.replace("/api/skill/", "");
|
|
@@ -1077,7 +1189,7 @@ class ViewerServer {
|
|
|
1077
1189
|
}
|
|
1078
1190
|
});
|
|
1079
1191
|
}
|
|
1080
|
-
handleSkillDelete(res, urlPath) {
|
|
1192
|
+
async handleSkillDelete(res, urlPath) {
|
|
1081
1193
|
const skillId = urlPath.replace("/api/skill/", "");
|
|
1082
1194
|
const skill = this.store.getSkill(skillId);
|
|
1083
1195
|
if (!skill) {
|
|
@@ -1085,7 +1197,18 @@ class ViewerServer {
|
|
|
1085
1197
|
res.end(JSON.stringify({ error: "Skill not found" }));
|
|
1086
1198
|
return;
|
|
1087
1199
|
}
|
|
1088
|
-
|
|
1200
|
+
try {
|
|
1201
|
+
const hub = this.resolveHubConnection();
|
|
1202
|
+
if (hub) {
|
|
1203
|
+
await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/skills/unpublish", {
|
|
1204
|
+
method: "POST",
|
|
1205
|
+
body: JSON.stringify({ sourceSkillId: skillId }),
|
|
1206
|
+
}).catch(() => { });
|
|
1207
|
+
}
|
|
1208
|
+
const db = this.store.db;
|
|
1209
|
+
db.prepare("DELETE FROM hub_skills WHERE source_skill_id = ?").run(skillId);
|
|
1210
|
+
}
|
|
1211
|
+
catch (_) { }
|
|
1089
1212
|
try {
|
|
1090
1213
|
if (skill.dirPath && node_fs_1.default.existsSync(skill.dirPath)) {
|
|
1091
1214
|
node_fs_1.default.rmSync(skill.dirPath, { recursive: true, force: true });
|
|
@@ -1135,7 +1258,15 @@ class ViewerServer {
|
|
|
1135
1258
|
const cleaned = chunk.role === "user" && chunk.content
|
|
1136
1259
|
? { ...chunk, content: (0, capture_1.stripInboundMetadata)(chunk.content) }
|
|
1137
1260
|
: chunk;
|
|
1138
|
-
this.
|
|
1261
|
+
const localShared = this.store.getLocalSharedMemory(chunkId);
|
|
1262
|
+
this.jsonResponse(res, {
|
|
1263
|
+
memory: {
|
|
1264
|
+
...cleaned,
|
|
1265
|
+
localSharing: cleaned.owner === "public",
|
|
1266
|
+
localSharingManaged: !!localShared,
|
|
1267
|
+
localOriginalOwner: localShared?.originalOwner ?? null,
|
|
1268
|
+
},
|
|
1269
|
+
});
|
|
1139
1270
|
}
|
|
1140
1271
|
handleUpdate(req, res, urlPath) {
|
|
1141
1272
|
const chunkId = urlPath.replace("/api/memory/", "");
|
|
@@ -1170,6 +1301,378 @@ class ViewerServer {
|
|
|
1170
1301
|
res.end(JSON.stringify({ error: "Not found" }));
|
|
1171
1302
|
}
|
|
1172
1303
|
}
|
|
1304
|
+
handleMemoryLocalShare(req, res) {
|
|
1305
|
+
this.readBody(req, (body) => {
|
|
1306
|
+
try {
|
|
1307
|
+
const parsed = JSON.parse(body || "{}");
|
|
1308
|
+
const chunkId = String(parsed.chunkId || "");
|
|
1309
|
+
if (!chunkId)
|
|
1310
|
+
return this.jsonResponse(res, { ok: false, error: "missing_chunk_id" }, 400);
|
|
1311
|
+
const result = this.store.markMemorySharedLocally(chunkId);
|
|
1312
|
+
if (!result.ok) {
|
|
1313
|
+
return this.jsonResponse(res, { ok: false, error: result.reason ?? "share_failed" }, result.reason === "not_found" ? 404 : 400);
|
|
1314
|
+
}
|
|
1315
|
+
this.jsonResponse(res, {
|
|
1316
|
+
ok: true,
|
|
1317
|
+
chunkId,
|
|
1318
|
+
owner: result.owner,
|
|
1319
|
+
localSharing: true,
|
|
1320
|
+
localSharingManaged: true,
|
|
1321
|
+
localOriginalOwner: result.originalOwner ?? null,
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
catch (err) {
|
|
1325
|
+
this.jsonResponse(res, { ok: false, error: String(err) }, 400);
|
|
1326
|
+
}
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1329
|
+
handleMemoryLocalUnshare(req, res) {
|
|
1330
|
+
this.readBody(req, (body) => {
|
|
1331
|
+
try {
|
|
1332
|
+
const parsed = JSON.parse(body || "{}");
|
|
1333
|
+
const chunkId = String(parsed.chunkId || "");
|
|
1334
|
+
const privateOwner = typeof parsed.privateOwner === "string" ? parsed.privateOwner : undefined;
|
|
1335
|
+
if (!chunkId)
|
|
1336
|
+
return this.jsonResponse(res, { ok: false, error: "missing_chunk_id" }, 400);
|
|
1337
|
+
const result = this.store.unmarkMemorySharedLocally(chunkId, privateOwner);
|
|
1338
|
+
if (!result.ok) {
|
|
1339
|
+
return this.jsonResponse(res, { ok: false, error: result.reason ?? "unshare_failed" }, result.reason === "not_found" ? 404 : 400);
|
|
1340
|
+
}
|
|
1341
|
+
this.jsonResponse(res, {
|
|
1342
|
+
ok: true,
|
|
1343
|
+
chunkId,
|
|
1344
|
+
owner: result.owner,
|
|
1345
|
+
localSharing: false,
|
|
1346
|
+
localOriginalOwner: result.originalOwner ?? null,
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
catch (err) {
|
|
1350
|
+
this.jsonResponse(res, { ok: false, error: String(err) }, 400);
|
|
1351
|
+
}
|
|
1352
|
+
});
|
|
1353
|
+
}
|
|
1354
|
+
// ─── Unified scope API ───
|
|
1355
|
+
handleMemoryScope(req, res, urlPath) {
|
|
1356
|
+
const chunkId = urlPath.split("/")[3];
|
|
1357
|
+
this.readBody(req, async (body) => {
|
|
1358
|
+
try {
|
|
1359
|
+
const parsed = JSON.parse(body || "{}");
|
|
1360
|
+
const scope = parsed.scope;
|
|
1361
|
+
if (!["private", "local", "team"].includes(scope)) {
|
|
1362
|
+
return this.jsonResponse(res, { ok: false, error: "scope must be 'private', 'local', or 'team'" }, 400);
|
|
1363
|
+
}
|
|
1364
|
+
const db = this.store.db;
|
|
1365
|
+
const chunk = db.prepare("SELECT * FROM chunks WHERE id = ?").get(chunkId);
|
|
1366
|
+
if (!chunk)
|
|
1367
|
+
return this.jsonResponse(res, { ok: false, error: "not_found" }, 404);
|
|
1368
|
+
if (chunk.dedup_status && chunk.dedup_status !== "active") {
|
|
1369
|
+
return this.jsonResponse(res, { ok: false, error: "inactive_memory", message: "Merged/duplicate memories cannot be shared" }, 400);
|
|
1370
|
+
}
|
|
1371
|
+
const isLocalShared = chunk.owner === "public";
|
|
1372
|
+
const hubMemory = this.getHubMemoryForChunk(chunkId);
|
|
1373
|
+
const isTeamShared = !!hubMemory;
|
|
1374
|
+
const currentScope = isTeamShared ? "team" : isLocalShared ? "local" : "private";
|
|
1375
|
+
if (scope === currentScope) {
|
|
1376
|
+
return this.jsonResponse(res, { ok: true, scope, changed: false });
|
|
1377
|
+
}
|
|
1378
|
+
let hubSynced = false;
|
|
1379
|
+
if (scope === "team") {
|
|
1380
|
+
if (!isTeamShared) {
|
|
1381
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1382
|
+
const refreshedChunk = db.prepare("SELECT * FROM chunks WHERE id = ?").get(chunkId);
|
|
1383
|
+
const response = await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/share", {
|
|
1384
|
+
method: "POST",
|
|
1385
|
+
body: JSON.stringify({ memory: { sourceChunkId: refreshedChunk.id, role: refreshedChunk.role, content: refreshedChunk.content, summary: refreshedChunk.summary, kind: refreshedChunk.kind, groupId: null, visibility: "public" } }),
|
|
1386
|
+
});
|
|
1387
|
+
if (!isLocalShared)
|
|
1388
|
+
this.store.markMemorySharedLocally(chunkId);
|
|
1389
|
+
const memoryId = String(response?.memoryId ?? "");
|
|
1390
|
+
const isHubRole = this.ctx?.config?.sharing?.role === "hub";
|
|
1391
|
+
if (hubClient.userId && isHubRole) {
|
|
1392
|
+
const existing = this.store.getHubMemoryBySource(hubClient.userId, chunkId);
|
|
1393
|
+
this.store.upsertHubMemory({
|
|
1394
|
+
id: memoryId || existing?.id || node_crypto_1.default.randomUUID(),
|
|
1395
|
+
sourceChunkId: chunkId, sourceUserId: hubClient.userId,
|
|
1396
|
+
role: refreshedChunk.role, content: refreshedChunk.content, summary: refreshedChunk.summary ?? "",
|
|
1397
|
+
kind: refreshedChunk.kind, groupId: null, visibility: "public",
|
|
1398
|
+
createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(),
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
else if (hubClient.userId) {
|
|
1402
|
+
this.store.upsertTeamSharedChunk(chunkId, { hubMemoryId: memoryId, visibility: "public", groupId: null });
|
|
1403
|
+
}
|
|
1404
|
+
hubSynced = true;
|
|
1405
|
+
}
|
|
1406
|
+
else {
|
|
1407
|
+
if (!isLocalShared)
|
|
1408
|
+
this.store.markMemorySharedLocally(chunkId);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
else if (scope === "local") {
|
|
1412
|
+
if (isTeamShared) {
|
|
1413
|
+
try {
|
|
1414
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1415
|
+
await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/unshare", {
|
|
1416
|
+
method: "POST", body: JSON.stringify({ sourceChunkId: chunkId }),
|
|
1417
|
+
});
|
|
1418
|
+
if (hubClient.userId)
|
|
1419
|
+
this.store.deleteHubMemoryBySource(hubClient.userId, chunkId);
|
|
1420
|
+
this.store.deleteTeamSharedChunk(chunkId);
|
|
1421
|
+
hubSynced = true;
|
|
1422
|
+
}
|
|
1423
|
+
catch (err) {
|
|
1424
|
+
this.log.warn(`Failed to unshare memory from team: ${err}`);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
if (!isLocalShared)
|
|
1428
|
+
this.store.markMemorySharedLocally(chunkId);
|
|
1429
|
+
}
|
|
1430
|
+
else {
|
|
1431
|
+
if (isTeamShared) {
|
|
1432
|
+
try {
|
|
1433
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1434
|
+
await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/unshare", {
|
|
1435
|
+
method: "POST", body: JSON.stringify({ sourceChunkId: chunkId }),
|
|
1436
|
+
});
|
|
1437
|
+
if (hubClient.userId)
|
|
1438
|
+
this.store.deleteHubMemoryBySource(hubClient.userId, chunkId);
|
|
1439
|
+
this.store.deleteTeamSharedChunk(chunkId);
|
|
1440
|
+
hubSynced = true;
|
|
1441
|
+
}
|
|
1442
|
+
catch (err) {
|
|
1443
|
+
this.log.warn(`Failed to unshare memory from team: ${err}`);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
if (isLocalShared)
|
|
1447
|
+
this.store.unmarkMemorySharedLocally(chunkId);
|
|
1448
|
+
}
|
|
1449
|
+
this.jsonResponse(res, { ok: true, scope, changed: true, hubSynced });
|
|
1450
|
+
}
|
|
1451
|
+
catch (err) {
|
|
1452
|
+
this.jsonResponse(res, { ok: false, error: String(err) }, 500);
|
|
1453
|
+
}
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
handleTaskScope(req, res, urlPath) {
|
|
1457
|
+
const taskId = urlPath.split("/")[3];
|
|
1458
|
+
this.readBody(req, async (body) => {
|
|
1459
|
+
try {
|
|
1460
|
+
const parsed = JSON.parse(body || "{}");
|
|
1461
|
+
const scope = parsed.scope;
|
|
1462
|
+
if (!["private", "local", "team"].includes(scope)) {
|
|
1463
|
+
return this.jsonResponse(res, { ok: false, error: "scope must be 'private', 'local', or 'team'" }, 400);
|
|
1464
|
+
}
|
|
1465
|
+
const task = this.store.getTask(taskId);
|
|
1466
|
+
if (!task)
|
|
1467
|
+
return this.jsonResponse(res, { ok: false, error: "task_not_found" }, 404);
|
|
1468
|
+
if (scope !== "private" && task.status !== "completed") {
|
|
1469
|
+
return this.jsonResponse(res, { ok: false, error: "only_completed_tasks_can_be_shared" }, 400);
|
|
1470
|
+
}
|
|
1471
|
+
const isLocalShared = task.owner === "public";
|
|
1472
|
+
const hubTask = this.getHubTaskForLocal(taskId);
|
|
1473
|
+
const isTeamShared = !!hubTask;
|
|
1474
|
+
const currentScope = isTeamShared ? "team" : isLocalShared ? "local" : "private";
|
|
1475
|
+
if (scope === currentScope) {
|
|
1476
|
+
return this.jsonResponse(res, { ok: true, scope, changed: false });
|
|
1477
|
+
}
|
|
1478
|
+
let hubSynced = false;
|
|
1479
|
+
if (scope === "team") {
|
|
1480
|
+
if (!isTeamShared) {
|
|
1481
|
+
const chunks = this.store.getChunksByTask(taskId);
|
|
1482
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1483
|
+
const refreshedTask = this.store.getTask(taskId);
|
|
1484
|
+
const response = await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/share", {
|
|
1485
|
+
method: "POST",
|
|
1486
|
+
body: JSON.stringify({
|
|
1487
|
+
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() },
|
|
1488
|
+
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() })),
|
|
1489
|
+
}),
|
|
1490
|
+
});
|
|
1491
|
+
if (hubClient.userId) {
|
|
1492
|
+
const existing = this.store.getHubTaskBySource(hubClient.userId, taskId);
|
|
1493
|
+
this.store.upsertHubTask({
|
|
1494
|
+
id: response?.taskId ?? existing?.id ?? node_crypto_1.default.randomUUID(),
|
|
1495
|
+
sourceTaskId: taskId, sourceUserId: hubClient.userId, title: refreshedTask.title ?? "",
|
|
1496
|
+
summary: refreshedTask.summary ?? "", groupId: null, visibility: "public",
|
|
1497
|
+
createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(),
|
|
1498
|
+
});
|
|
1499
|
+
}
|
|
1500
|
+
hubSynced = true;
|
|
1501
|
+
}
|
|
1502
|
+
if (!isLocalShared) {
|
|
1503
|
+
const originalOwner = task.owner;
|
|
1504
|
+
const db = this.store.db;
|
|
1505
|
+
db.prepare("INSERT INTO local_shared_tasks (task_id, hub_task_id, original_owner, shared_at) VALUES (?, ?, ?, ?) ON CONFLICT(task_id) DO UPDATE SET original_owner = excluded.original_owner, shared_at = excluded.shared_at").run(taskId, "", originalOwner, Date.now());
|
|
1506
|
+
db.prepare("UPDATE tasks SET owner = 'public' WHERE id = ?").run(taskId);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
if (scope === "local") {
|
|
1510
|
+
if (!isLocalShared) {
|
|
1511
|
+
const originalOwner = task.owner;
|
|
1512
|
+
const db = this.store.db;
|
|
1513
|
+
db.prepare("INSERT INTO local_shared_tasks (task_id, hub_task_id, original_owner, shared_at) VALUES (?, ?, ?, ?) ON CONFLICT(task_id) DO UPDATE SET original_owner = excluded.original_owner, shared_at = excluded.shared_at").run(taskId, "", originalOwner, Date.now());
|
|
1514
|
+
db.prepare("UPDATE tasks SET owner = 'public' WHERE id = ?").run(taskId);
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
if (scope === "local" && isTeamShared) {
|
|
1518
|
+
try {
|
|
1519
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1520
|
+
await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/unshare", {
|
|
1521
|
+
method: "POST", body: JSON.stringify({ sourceTaskId: taskId }),
|
|
1522
|
+
});
|
|
1523
|
+
if (hubClient.userId)
|
|
1524
|
+
this.store.deleteHubTaskBySource(hubClient.userId, taskId);
|
|
1525
|
+
hubSynced = true;
|
|
1526
|
+
}
|
|
1527
|
+
catch (err) {
|
|
1528
|
+
this.log.warn(`Failed to unshare task from team: ${err}`);
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
if (scope === "private") {
|
|
1532
|
+
if (isTeamShared) {
|
|
1533
|
+
try {
|
|
1534
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1535
|
+
await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/unshare", {
|
|
1536
|
+
method: "POST", body: JSON.stringify({ sourceTaskId: taskId }),
|
|
1537
|
+
});
|
|
1538
|
+
if (hubClient.userId)
|
|
1539
|
+
this.store.deleteHubTaskBySource(hubClient.userId, taskId);
|
|
1540
|
+
hubSynced = true;
|
|
1541
|
+
}
|
|
1542
|
+
catch (err) {
|
|
1543
|
+
this.log.warn(`Failed to unshare task from team: ${err}`);
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
if (isLocalShared) {
|
|
1547
|
+
const db = this.store.db;
|
|
1548
|
+
const shared = db.prepare("SELECT original_owner FROM local_shared_tasks WHERE task_id = ?").get(taskId);
|
|
1549
|
+
const restoreOwner = shared?.original_owner ?? task.owner;
|
|
1550
|
+
if (restoreOwner && restoreOwner !== "public") {
|
|
1551
|
+
db.prepare("UPDATE tasks SET owner = ? WHERE id = ?").run(restoreOwner, taskId);
|
|
1552
|
+
}
|
|
1553
|
+
db.prepare("DELETE FROM local_shared_tasks WHERE task_id = ?").run(taskId);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
this.jsonResponse(res, { ok: true, scope, changed: true, hubSynced });
|
|
1557
|
+
}
|
|
1558
|
+
catch (err) {
|
|
1559
|
+
this.jsonResponse(res, { ok: false, error: String(err) }, 500);
|
|
1560
|
+
}
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
handleSkillScope(req, res, urlPath) {
|
|
1564
|
+
const skillId = urlPath.split("/")[3];
|
|
1565
|
+
this.readBody(req, async (body) => {
|
|
1566
|
+
try {
|
|
1567
|
+
const parsed = JSON.parse(body || "{}");
|
|
1568
|
+
const scope = parsed.scope;
|
|
1569
|
+
if (!["private", "local", "team"].includes(scope)) {
|
|
1570
|
+
return this.jsonResponse(res, { ok: false, error: "scope must be 'private', 'local', or 'team'" }, 400);
|
|
1571
|
+
}
|
|
1572
|
+
const skill = this.store.getSkill(skillId);
|
|
1573
|
+
if (!skill)
|
|
1574
|
+
return this.jsonResponse(res, { ok: false, error: "skill_not_found" }, 404);
|
|
1575
|
+
if (scope !== "private" && skill.status !== "active") {
|
|
1576
|
+
return this.jsonResponse(res, { ok: false, error: "only_active_skills_can_be_shared" }, 400);
|
|
1577
|
+
}
|
|
1578
|
+
const isLocalShared = skill.visibility === "public";
|
|
1579
|
+
const hubSkill = this.getHubSkillForLocal(skillId);
|
|
1580
|
+
const isTeamShared = !!hubSkill;
|
|
1581
|
+
const currentScope = isTeamShared ? "team" : isLocalShared ? "local" : "private";
|
|
1582
|
+
if (scope === currentScope) {
|
|
1583
|
+
return this.jsonResponse(res, { ok: true, scope, changed: false });
|
|
1584
|
+
}
|
|
1585
|
+
let hubSynced = false;
|
|
1586
|
+
if (scope === "team") {
|
|
1587
|
+
if (!isTeamShared) {
|
|
1588
|
+
const bundle = (0, skill_sync_1.buildSkillBundleForHub)(this.store, skillId);
|
|
1589
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1590
|
+
const response = await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/publish", {
|
|
1591
|
+
method: "POST",
|
|
1592
|
+
body: JSON.stringify({ visibility: "public", groupId: null, metadata: bundle.metadata, bundle: bundle.bundle }),
|
|
1593
|
+
});
|
|
1594
|
+
if (hubClient.userId) {
|
|
1595
|
+
const existing = this.store.getHubSkillBySource(hubClient.userId, skillId);
|
|
1596
|
+
this.store.upsertHubSkill({
|
|
1597
|
+
id: response?.skillId ?? existing?.id ?? node_crypto_1.default.randomUUID(),
|
|
1598
|
+
sourceSkillId: skillId, sourceUserId: hubClient.userId,
|
|
1599
|
+
name: skill.name, description: skill.description, version: skill.version,
|
|
1600
|
+
groupId: null, visibility: "public",
|
|
1601
|
+
bundle: JSON.stringify(bundle.bundle), qualityScore: skill.qualityScore,
|
|
1602
|
+
createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(),
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
hubSynced = true;
|
|
1606
|
+
}
|
|
1607
|
+
if (!isLocalShared)
|
|
1608
|
+
this.store.setSkillVisibility(skillId, "public");
|
|
1609
|
+
}
|
|
1610
|
+
if (scope === "local") {
|
|
1611
|
+
if (!isLocalShared)
|
|
1612
|
+
this.store.setSkillVisibility(skillId, "public");
|
|
1613
|
+
}
|
|
1614
|
+
if (scope === "local" && isTeamShared) {
|
|
1615
|
+
try {
|
|
1616
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1617
|
+
await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/unpublish", {
|
|
1618
|
+
method: "POST", body: JSON.stringify({ sourceSkillId: skillId }),
|
|
1619
|
+
});
|
|
1620
|
+
if (hubClient.userId)
|
|
1621
|
+
this.store.deleteHubSkillBySource(hubClient.userId, skillId);
|
|
1622
|
+
hubSynced = true;
|
|
1623
|
+
}
|
|
1624
|
+
catch (err) {
|
|
1625
|
+
this.log.warn(`Failed to unpublish skill from team: ${err}`);
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
if (scope === "private") {
|
|
1629
|
+
if (isTeamShared) {
|
|
1630
|
+
try {
|
|
1631
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1632
|
+
await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/unpublish", {
|
|
1633
|
+
method: "POST", body: JSON.stringify({ sourceSkillId: skillId }),
|
|
1634
|
+
});
|
|
1635
|
+
if (hubClient.userId)
|
|
1636
|
+
this.store.deleteHubSkillBySource(hubClient.userId, skillId);
|
|
1637
|
+
hubSynced = true;
|
|
1638
|
+
}
|
|
1639
|
+
catch (err) {
|
|
1640
|
+
this.log.warn(`Failed to unpublish skill from team: ${err}`);
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
if (isLocalShared)
|
|
1644
|
+
this.store.setSkillVisibility(skillId, "private");
|
|
1645
|
+
}
|
|
1646
|
+
this.jsonResponse(res, { ok: true, scope, changed: true, hubSynced });
|
|
1647
|
+
}
|
|
1648
|
+
catch (err) {
|
|
1649
|
+
this.jsonResponse(res, { ok: false, error: String(err) }, 500);
|
|
1650
|
+
}
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
getHubMemoryForChunk(chunkId) {
|
|
1654
|
+
const db = this.store.db;
|
|
1655
|
+
const hub = db.prepare("SELECT * FROM hub_memories WHERE source_chunk_id = ? LIMIT 1").get(chunkId);
|
|
1656
|
+
if (hub)
|
|
1657
|
+
return hub;
|
|
1658
|
+
const ts = this.store.getTeamSharedChunk(chunkId);
|
|
1659
|
+
if (ts) {
|
|
1660
|
+
return {
|
|
1661
|
+
source_chunk_id: chunkId,
|
|
1662
|
+
visibility: ts.visibility,
|
|
1663
|
+
group_id: ts.groupId,
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
return undefined;
|
|
1667
|
+
}
|
|
1668
|
+
getHubTaskForLocal(taskId) {
|
|
1669
|
+
const db = this.store.db;
|
|
1670
|
+
return db.prepare("SELECT * FROM hub_tasks WHERE source_task_id = ? LIMIT 1").get(taskId);
|
|
1671
|
+
}
|
|
1672
|
+
getHubSkillForLocal(skillId) {
|
|
1673
|
+
const db = this.store.db;
|
|
1674
|
+
return db.prepare("SELECT * FROM hub_skills WHERE source_skill_id = ? LIMIT 1").get(skillId);
|
|
1675
|
+
}
|
|
1173
1676
|
handleDeleteSession(res, url) {
|
|
1174
1677
|
const key = url.searchParams.get("key");
|
|
1175
1678
|
if (!key) {
|
|
@@ -1206,8 +1709,11 @@ class ViewerServer {
|
|
|
1206
1709
|
// ─── Helpers ───
|
|
1207
1710
|
// ─── Config API ───
|
|
1208
1711
|
getOpenClawConfigPath() {
|
|
1712
|
+
if (process.env.OPENCLAW_CONFIG_PATH)
|
|
1713
|
+
return process.env.OPENCLAW_CONFIG_PATH;
|
|
1209
1714
|
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
1210
|
-
|
|
1715
|
+
const ocHome = process.env.OPENCLAW_STATE_DIR || node_path_1.default.join(home, ".openclaw");
|
|
1716
|
+
return node_path_1.default.join(ocHome, "openclaw.json");
|
|
1211
1717
|
}
|
|
1212
1718
|
getPluginEntryConfig(raw) {
|
|
1213
1719
|
const entries = raw?.plugins?.entries ?? {};
|
|
@@ -1263,8 +1769,7 @@ class ViewerServer {
|
|
|
1263
1769
|
base.admin.rejectSupported = true;
|
|
1264
1770
|
base.connection.connected = true;
|
|
1265
1771
|
base.connection.hubUrl = resolvedHubUrl ?? undefined;
|
|
1266
|
-
|
|
1267
|
-
let adminUser = { username: "hub-admin", role: "admin", groups: [] };
|
|
1772
|
+
let adminUser = { username: "hub-admin", role: "admin" };
|
|
1268
1773
|
try {
|
|
1269
1774
|
const hub = this.resolveHubConnection();
|
|
1270
1775
|
if (hub) {
|
|
@@ -1274,7 +1779,6 @@ class ViewerServer {
|
|
|
1274
1779
|
id: me.id,
|
|
1275
1780
|
username: me.username ?? "hub-admin",
|
|
1276
1781
|
role: me.role ?? "admin",
|
|
1277
|
-
groups: Array.isArray(me.groups) ? me.groups : [],
|
|
1278
1782
|
};
|
|
1279
1783
|
}
|
|
1280
1784
|
}
|
|
@@ -1289,7 +1793,18 @@ class ViewerServer {
|
|
|
1289
1793
|
base.connection.apiVersion = info?.apiVersion ?? null;
|
|
1290
1794
|
}
|
|
1291
1795
|
catch { /* ignore */ }
|
|
1292
|
-
|
|
1796
|
+
const hubStats = { totalMembers: 0, onlineMembers: 0, pendingMembers: 0 };
|
|
1797
|
+
try {
|
|
1798
|
+
const activeUsers = this.store.listHubUsers("active");
|
|
1799
|
+
const pendingUsers = this.store.listHubUsers("pending");
|
|
1800
|
+
const now = Date.now();
|
|
1801
|
+
const OFFLINE_THRESHOLD = 120_000;
|
|
1802
|
+
hubStats.totalMembers = activeUsers.length;
|
|
1803
|
+
hubStats.onlineMembers = activeUsers.filter(u => u.lastActiveAt && (now - u.lastActiveAt < OFFLINE_THRESHOLD)).length;
|
|
1804
|
+
hubStats.pendingMembers = pendingUsers.length;
|
|
1805
|
+
}
|
|
1806
|
+
catch { /* best-effort */ }
|
|
1807
|
+
this.jsonResponse(res, { ...base, hubStats });
|
|
1293
1808
|
return;
|
|
1294
1809
|
}
|
|
1295
1810
|
const hasPendingConnection = Boolean(persisted?.hubUrl && persisted?.userId && !persisted?.userToken);
|
|
@@ -1306,6 +1821,9 @@ class ViewerServer {
|
|
|
1306
1821
|
if (status.user?.status === "rejected") {
|
|
1307
1822
|
output.connection.rejected = true;
|
|
1308
1823
|
}
|
|
1824
|
+
if (status.user?.status === "removed") {
|
|
1825
|
+
output.connection.removed = true;
|
|
1826
|
+
}
|
|
1309
1827
|
if (status.connected && status.hubUrl) {
|
|
1310
1828
|
try {
|
|
1311
1829
|
const info = await fetch(`${status.hubUrl}/api/v1/hub/info`).then((r) => (r.ok ? r.json() : null)).catch(() => null);
|
|
@@ -1383,6 +1901,75 @@ class ViewerServer {
|
|
|
1383
1901
|
}
|
|
1384
1902
|
});
|
|
1385
1903
|
}
|
|
1904
|
+
handleSharingChangeRole(req, res) {
|
|
1905
|
+
this.readBody(req, async (body) => {
|
|
1906
|
+
if (!this.ctx)
|
|
1907
|
+
return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
1908
|
+
try {
|
|
1909
|
+
const parsed = JSON.parse(body || "{}");
|
|
1910
|
+
const hub = this.resolveHubConnection();
|
|
1911
|
+
if (!hub)
|
|
1912
|
+
return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1913
|
+
const result = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/change-role", {
|
|
1914
|
+
method: "POST",
|
|
1915
|
+
body: JSON.stringify({ userId: parsed.userId, role: parsed.role }),
|
|
1916
|
+
});
|
|
1917
|
+
this.jsonResponse(res, { ok: true, result });
|
|
1918
|
+
}
|
|
1919
|
+
catch (err) {
|
|
1920
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1921
|
+
}
|
|
1922
|
+
});
|
|
1923
|
+
}
|
|
1924
|
+
handleSharingRemoveUser(req, res) {
|
|
1925
|
+
this.readBody(req, async (body) => {
|
|
1926
|
+
if (!this.ctx)
|
|
1927
|
+
return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
1928
|
+
try {
|
|
1929
|
+
const parsed = JSON.parse(body || "{}");
|
|
1930
|
+
const hub = this.resolveHubConnection();
|
|
1931
|
+
if (!hub)
|
|
1932
|
+
return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1933
|
+
const result = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/remove-user", {
|
|
1934
|
+
method: "POST",
|
|
1935
|
+
body: JSON.stringify({ userId: parsed.userId, cleanResources: parsed.cleanResources === true }),
|
|
1936
|
+
});
|
|
1937
|
+
this.jsonResponse(res, { ok: true, result });
|
|
1938
|
+
}
|
|
1939
|
+
catch (err) {
|
|
1940
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1941
|
+
}
|
|
1942
|
+
});
|
|
1943
|
+
}
|
|
1944
|
+
handleAdminRenameUser(req, res) {
|
|
1945
|
+
this.readBody(req, async (body) => {
|
|
1946
|
+
if (!this.ctx)
|
|
1947
|
+
return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
1948
|
+
try {
|
|
1949
|
+
const parsed = JSON.parse(body || "{}");
|
|
1950
|
+
const hub = this.resolveHubConnection();
|
|
1951
|
+
if (!hub)
|
|
1952
|
+
return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1953
|
+
const result = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/rename-user", {
|
|
1954
|
+
method: "POST",
|
|
1955
|
+
body: JSON.stringify({ userId: parsed.userId, username: parsed.username }),
|
|
1956
|
+
});
|
|
1957
|
+
this.jsonResponse(res, { ok: true, result });
|
|
1958
|
+
}
|
|
1959
|
+
catch (err) {
|
|
1960
|
+
const errStr = String(err);
|
|
1961
|
+
if (errStr.includes("username_taken")) {
|
|
1962
|
+
this.jsonResponse(res, { ok: false, error: "username_taken" });
|
|
1963
|
+
}
|
|
1964
|
+
else if (errStr.includes("invalid_params")) {
|
|
1965
|
+
this.jsonResponse(res, { ok: false, error: "invalid_params" });
|
|
1966
|
+
}
|
|
1967
|
+
else {
|
|
1968
|
+
this.jsonResponse(res, { ok: false, error: errStr });
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
});
|
|
1972
|
+
}
|
|
1386
1973
|
handleRetryJoin(req, res) {
|
|
1387
1974
|
this.readBody(req, async (_body) => {
|
|
1388
1975
|
if (!this.ctx)
|
|
@@ -1399,12 +1986,16 @@ class ViewerServer {
|
|
|
1399
1986
|
try {
|
|
1400
1987
|
const hubUrl = (0, hub_1.normalizeHubUrl)(hubAddress);
|
|
1401
1988
|
const os = await Promise.resolve().then(() => __importStar(require("os")));
|
|
1402
|
-
const
|
|
1989
|
+
const nickname = sharing.client?.nickname;
|
|
1990
|
+
const username = nickname || os.userInfo().username || "user";
|
|
1403
1991
|
const hostname = os.hostname() || "unknown";
|
|
1992
|
+
const persisted = this.store.getClientHubConnection();
|
|
1993
|
+
const existingIdentityKey = persisted?.identityKey || "";
|
|
1404
1994
|
const result = await (0, hub_1.hubRequestJson)(hubUrl, "", "/api/v1/hub/join", {
|
|
1405
1995
|
method: "POST",
|
|
1406
|
-
body: JSON.stringify({ teamToken, username, deviceName: hostname }),
|
|
1996
|
+
body: JSON.stringify({ teamToken, username, deviceName: hostname, reapply: true, identityKey: existingIdentityKey }),
|
|
1407
1997
|
});
|
|
1998
|
+
const returnedIdentityKey = String(result.identityKey || existingIdentityKey || "");
|
|
1408
1999
|
this.store.setClientHubConnection({
|
|
1409
2000
|
hubUrl,
|
|
1410
2001
|
userId: String(result.userId || ""),
|
|
@@ -1412,6 +2003,8 @@ class ViewerServer {
|
|
|
1412
2003
|
userToken: result.userToken || "",
|
|
1413
2004
|
role: "member",
|
|
1414
2005
|
connectedAt: Date.now(),
|
|
2006
|
+
identityKey: returnedIdentityKey,
|
|
2007
|
+
lastKnownStatus: result.status || "",
|
|
1415
2008
|
});
|
|
1416
2009
|
this.jsonResponse(res, { ok: true, status: result.status || "pending" });
|
|
1417
2010
|
}
|
|
@@ -1425,7 +2018,14 @@ class ViewerServer {
|
|
|
1425
2018
|
return this.jsonResponse(res, { memories: [], error: "sharing_unavailable" });
|
|
1426
2019
|
try {
|
|
1427
2020
|
const limit = Number(url.searchParams.get("limit") || 40);
|
|
1428
|
-
const
|
|
2021
|
+
const hub = this.resolveHubConnection();
|
|
2022
|
+
let data;
|
|
2023
|
+
if (hub) {
|
|
2024
|
+
data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/memories?limit=${limit}`);
|
|
2025
|
+
}
|
|
2026
|
+
else {
|
|
2027
|
+
data = await (0, hub_1.hubListMemories)(this.store, this.ctx, { limit });
|
|
2028
|
+
}
|
|
1429
2029
|
this.jsonResponse(res, { memories: Array.isArray(data?.memories) ? data.memories : [] });
|
|
1430
2030
|
}
|
|
1431
2031
|
catch (err) {
|
|
@@ -1437,7 +2037,14 @@ class ViewerServer {
|
|
|
1437
2037
|
return this.jsonResponse(res, { tasks: [], error: "sharing_unavailable" });
|
|
1438
2038
|
try {
|
|
1439
2039
|
const limit = Number(url.searchParams.get("limit") || 40);
|
|
1440
|
-
const
|
|
2040
|
+
const hub = this.resolveHubConnection();
|
|
2041
|
+
let data;
|
|
2042
|
+
if (hub) {
|
|
2043
|
+
data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/tasks?limit=${limit}`);
|
|
2044
|
+
}
|
|
2045
|
+
else {
|
|
2046
|
+
data = await (0, hub_1.hubListTasks)(this.store, this.ctx, { limit });
|
|
2047
|
+
}
|
|
1441
2048
|
this.jsonResponse(res, { tasks: Array.isArray(data?.tasks) ? data.tasks : [] });
|
|
1442
2049
|
}
|
|
1443
2050
|
catch (err) {
|
|
@@ -1449,10 +2056,17 @@ class ViewerServer {
|
|
|
1449
2056
|
return this.jsonResponse(res, { skills: [], error: "sharing_unavailable" });
|
|
1450
2057
|
try {
|
|
1451
2058
|
const limit = Number(url.searchParams.get("limit") || 40);
|
|
1452
|
-
const
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
2059
|
+
const hub = this.resolveHubConnection();
|
|
2060
|
+
let data;
|
|
2061
|
+
if (hub) {
|
|
2062
|
+
data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/skills/list?limit=${limit}`);
|
|
2063
|
+
}
|
|
2064
|
+
else {
|
|
2065
|
+
data = await (0, hub_1.hubListSkills)(this.store, this.ctx, { limit });
|
|
2066
|
+
}
|
|
2067
|
+
this.jsonResponse(res, { skills: Array.isArray(data?.skills) ? data.skills : [] });
|
|
2068
|
+
}
|
|
2069
|
+
catch (err) {
|
|
1456
2070
|
this.jsonResponse(res, { skills: [], error: String(err) });
|
|
1457
2071
|
}
|
|
1458
2072
|
}
|
|
@@ -1466,13 +2080,22 @@ class ViewerServer {
|
|
|
1466
2080
|
const query = String(parsed.query || "");
|
|
1467
2081
|
const role = typeof parsed.role === "string" ? parsed.role : undefined;
|
|
1468
2082
|
const maxResults = typeof parsed.maxResults === "number" ? parsed.maxResults : 10;
|
|
1469
|
-
const scope = parsed.scope === "group" || parsed.scope === "all" ? parsed.scope : "local";
|
|
2083
|
+
const scope = parsed.scope === "group" || parsed.scope === "all" || parsed.scope === "hub" ? (parsed.scope === "hub" ? "all" : parsed.scope) : "local";
|
|
1470
2084
|
const local = this.searchLocalViewerMemories(query, { role, maxResults });
|
|
1471
2085
|
if (scope === "local") {
|
|
1472
2086
|
return this.jsonResponse(res, { local: { hits: local.hits, meta: local.meta }, hub: emptyHub });
|
|
1473
2087
|
}
|
|
1474
2088
|
try {
|
|
1475
|
-
const
|
|
2089
|
+
const conn = this.resolveHubConnection();
|
|
2090
|
+
let hub;
|
|
2091
|
+
if (conn) {
|
|
2092
|
+
hub = await (0, hub_1.hubRequestJson)(conn.hubUrl, conn.userToken, "/api/v1/hub/search", {
|
|
2093
|
+
method: "POST", body: JSON.stringify({ query, maxResults, scope }),
|
|
2094
|
+
});
|
|
2095
|
+
}
|
|
2096
|
+
else {
|
|
2097
|
+
hub = await (0, hub_1.hubSearchMemories)(this.store, this.ctx, { query, maxResults, scope });
|
|
2098
|
+
}
|
|
1476
2099
|
this.jsonResponse(res, { local: { hits: local.hits, meta: local.meta }, hub });
|
|
1477
2100
|
}
|
|
1478
2101
|
catch (err) {
|
|
@@ -1677,14 +2300,14 @@ class ViewerServer {
|
|
|
1677
2300
|
},
|
|
1678
2301
|
}),
|
|
1679
2302
|
});
|
|
1680
|
-
const
|
|
1681
|
-
if (
|
|
2303
|
+
const mid = String(response?.memoryId ?? "");
|
|
2304
|
+
if (hubClient.userId && this.ctx?.config?.sharing?.role === "hub") {
|
|
1682
2305
|
const now = Date.now();
|
|
1683
|
-
const existing = this.store.getHubMemoryBySource(
|
|
2306
|
+
const existing = this.store.getHubMemoryBySource(hubClient.userId, chunk.id);
|
|
1684
2307
|
this.store.upsertHubMemory({
|
|
1685
|
-
id:
|
|
2308
|
+
id: mid || existing?.id || node_crypto_1.default.randomUUID(),
|
|
1686
2309
|
sourceChunkId: chunk.id,
|
|
1687
|
-
sourceUserId:
|
|
2310
|
+
sourceUserId: hubClient.userId,
|
|
1688
2311
|
role: chunk.role,
|
|
1689
2312
|
content: chunk.content,
|
|
1690
2313
|
summary: chunk.summary ?? "",
|
|
@@ -1695,6 +2318,9 @@ class ViewerServer {
|
|
|
1695
2318
|
updatedAt: now,
|
|
1696
2319
|
});
|
|
1697
2320
|
}
|
|
2321
|
+
else if (hubClient.userId) {
|
|
2322
|
+
this.store.upsertTeamSharedChunk(chunk.id, { hubMemoryId: mid, visibility, groupId });
|
|
2323
|
+
}
|
|
1698
2324
|
this.jsonResponse(res, { ok: true, chunkId, visibility, response });
|
|
1699
2325
|
}
|
|
1700
2326
|
catch (err) {
|
|
@@ -1717,6 +2343,7 @@ class ViewerServer {
|
|
|
1717
2343
|
const hubUserId = hubClient.userId;
|
|
1718
2344
|
if (hubUserId)
|
|
1719
2345
|
this.store.deleteHubMemoryBySource(hubUserId, chunkId);
|
|
2346
|
+
this.store.deleteTeamSharedChunk(chunkId);
|
|
1720
2347
|
this.jsonResponse(res, { ok: true, chunkId });
|
|
1721
2348
|
}
|
|
1722
2349
|
catch (err) {
|
|
@@ -1819,7 +2446,7 @@ class ViewerServer {
|
|
|
1819
2446
|
// Hub 模式:连接自己,用 bootstrap admin token
|
|
1820
2447
|
const sharing = this.ctx.config.sharing;
|
|
1821
2448
|
if (sharing?.role === "hub") {
|
|
1822
|
-
const hubPort =
|
|
2449
|
+
const hubPort = this.getHubPort();
|
|
1823
2450
|
const hubUrl = `http://127.0.0.1:${hubPort}`;
|
|
1824
2451
|
try {
|
|
1825
2452
|
const authPath = node_path_1.default.join(this.dataDir, "hub-auth.json");
|
|
@@ -1860,123 +2487,6 @@ class ViewerServer {
|
|
|
1860
2487
|
}
|
|
1861
2488
|
return (0, hub_1.resolveHubClient)(this.store, this.ctx);
|
|
1862
2489
|
}
|
|
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
2490
|
async serveSharingUsers(res) {
|
|
1981
2491
|
const hub = this.resolveHubConnection();
|
|
1982
2492
|
if (!hub)
|
|
@@ -1996,7 +2506,17 @@ class ViewerServer {
|
|
|
1996
2506
|
return this.jsonResponse(res, { tasks: [], error: "not_configured" });
|
|
1997
2507
|
try {
|
|
1998
2508
|
const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/shared-tasks", { method: "GET" });
|
|
1999
|
-
|
|
2509
|
+
const tasks = Array.isArray(data?.tasks) ? data.tasks : [];
|
|
2510
|
+
for (const tk of tasks) {
|
|
2511
|
+
if (!tk.summary && tk.sourceTaskId) {
|
|
2512
|
+
const local = this.store.getTask(tk.sourceTaskId);
|
|
2513
|
+
if (local) {
|
|
2514
|
+
tk.summary = local.summary;
|
|
2515
|
+
tk.title = tk.title || local.title;
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
this.jsonResponse(res, { tasks });
|
|
2000
2520
|
}
|
|
2001
2521
|
catch (err) {
|
|
2002
2522
|
this.jsonResponse(res, { tasks: [], error: String(err) });
|
|
@@ -2015,13 +2535,55 @@ class ViewerServer {
|
|
|
2015
2535
|
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
2016
2536
|
}
|
|
2017
2537
|
}
|
|
2538
|
+
async serveHubTaskDetail(res, p) {
|
|
2539
|
+
const hub = this.resolveHubConnection();
|
|
2540
|
+
if (!hub)
|
|
2541
|
+
return this.jsonResponse(res, { error: "not_configured" }, 500);
|
|
2542
|
+
const m = p.match(/^\/api\/admin\/shared-tasks\/([^/]+)\/detail$/);
|
|
2543
|
+
if (!m)
|
|
2544
|
+
return this.jsonResponse(res, { error: "bad_request" }, 400);
|
|
2545
|
+
const taskId = decodeURIComponent(m[1]);
|
|
2546
|
+
try {
|
|
2547
|
+
const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/shared-tasks/${encodeURIComponent(taskId)}/detail`, { method: "GET" });
|
|
2548
|
+
this.jsonResponse(res, data);
|
|
2549
|
+
}
|
|
2550
|
+
catch (err) {
|
|
2551
|
+
this.jsonResponse(res, { error: String(err) }, 500);
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
async serveHubSkillDetail(res, p) {
|
|
2555
|
+
const hub = this.resolveHubConnection();
|
|
2556
|
+
if (!hub)
|
|
2557
|
+
return this.jsonResponse(res, { error: "not_configured" }, 500);
|
|
2558
|
+
const m = p.match(/^\/api\/admin\/shared-skills\/([^/]+)\/detail$/);
|
|
2559
|
+
if (!m)
|
|
2560
|
+
return this.jsonResponse(res, { error: "bad_request" }, 400);
|
|
2561
|
+
const skillId = decodeURIComponent(m[1]);
|
|
2562
|
+
try {
|
|
2563
|
+
const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/shared-skills/${encodeURIComponent(skillId)}/detail`, { method: "GET" });
|
|
2564
|
+
this.jsonResponse(res, data);
|
|
2565
|
+
}
|
|
2566
|
+
catch (err) {
|
|
2567
|
+
this.jsonResponse(res, { error: String(err) }, 500);
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2018
2570
|
async serveAdminSharedSkills(res) {
|
|
2019
2571
|
const hub = this.resolveHubConnection();
|
|
2020
2572
|
if (!hub)
|
|
2021
2573
|
return this.jsonResponse(res, { skills: [], error: "not_configured" });
|
|
2022
2574
|
try {
|
|
2023
2575
|
const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/shared-skills", { method: "GET" });
|
|
2024
|
-
|
|
2576
|
+
const skills = Array.isArray(data?.skills) ? data.skills : [];
|
|
2577
|
+
for (const sk of skills) {
|
|
2578
|
+
if (!sk.description && sk.sourceSkillId) {
|
|
2579
|
+
const local = this.store.getSkill(sk.sourceSkillId);
|
|
2580
|
+
if (local) {
|
|
2581
|
+
sk.description = sk.description || local.description;
|
|
2582
|
+
sk.name = sk.name || local.name;
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
this.jsonResponse(res, { skills });
|
|
2025
2587
|
}
|
|
2026
2588
|
catch (err) {
|
|
2027
2589
|
this.jsonResponse(res, { skills: [], error: String(err) });
|
|
@@ -2046,7 +2608,18 @@ class ViewerServer {
|
|
|
2046
2608
|
return this.jsonResponse(res, { memories: [], error: "not_configured" });
|
|
2047
2609
|
try {
|
|
2048
2610
|
const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/shared-memories", { method: "GET" });
|
|
2049
|
-
|
|
2611
|
+
const memories = Array.isArray(data?.memories) ? data.memories : [];
|
|
2612
|
+
for (const m of memories) {
|
|
2613
|
+
if (!m.content && m.sourceChunkId) {
|
|
2614
|
+
const local = this.store.getChunk(m.sourceChunkId);
|
|
2615
|
+
if (local) {
|
|
2616
|
+
m.content = local.content;
|
|
2617
|
+
if (!m.summary && local.summary)
|
|
2618
|
+
m.summary = local.summary;
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
this.jsonResponse(res, { memories });
|
|
2050
2623
|
}
|
|
2051
2624
|
catch (err) {
|
|
2052
2625
|
this.jsonResponse(res, { memories: [], error: String(err) });
|
|
@@ -2065,6 +2638,167 @@ class ViewerServer {
|
|
|
2065
2638
|
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
2066
2639
|
}
|
|
2067
2640
|
}
|
|
2641
|
+
async serveSharingNotifications(res, url) {
|
|
2642
|
+
const hub = this.resolveHubConnection();
|
|
2643
|
+
if (!hub)
|
|
2644
|
+
return this.jsonResponse(res, { notifications: [], unreadCount: 0 });
|
|
2645
|
+
try {
|
|
2646
|
+
const unread = url.searchParams.get("unread") === "1" ? "?unread=1" : "";
|
|
2647
|
+
const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/notifications${unread}`);
|
|
2648
|
+
this.jsonResponse(res, data);
|
|
2649
|
+
}
|
|
2650
|
+
catch {
|
|
2651
|
+
this.jsonResponse(res, { notifications: [], unreadCount: 0 });
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
handleSharingNotificationsRead(req, res) {
|
|
2655
|
+
const hub = this.resolveHubConnection();
|
|
2656
|
+
if (!hub)
|
|
2657
|
+
return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
2658
|
+
this.readBody(req, async (raw) => {
|
|
2659
|
+
try {
|
|
2660
|
+
const body = JSON.parse(raw || "{}");
|
|
2661
|
+
await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications/read", { method: "POST", body: JSON.stringify(body) });
|
|
2662
|
+
this.jsonResponse(res, { ok: true });
|
|
2663
|
+
try {
|
|
2664
|
+
const data = (await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications?unread=1"));
|
|
2665
|
+
const count = data?.unreadCount ?? 0;
|
|
2666
|
+
this.lastKnownNotifCount = count;
|
|
2667
|
+
this.broadcastNotifSSE({ type: "update", unreadCount: count });
|
|
2668
|
+
}
|
|
2669
|
+
catch { /* best effort */ }
|
|
2670
|
+
}
|
|
2671
|
+
catch (err) {
|
|
2672
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
2673
|
+
}
|
|
2674
|
+
});
|
|
2675
|
+
}
|
|
2676
|
+
handleSharingNotificationsClear(req, res) {
|
|
2677
|
+
const hub = this.resolveHubConnection();
|
|
2678
|
+
if (!hub)
|
|
2679
|
+
return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
2680
|
+
this.readBody(req, async () => {
|
|
2681
|
+
try {
|
|
2682
|
+
await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications/clear", { method: "POST", body: "{}" });
|
|
2683
|
+
this.jsonResponse(res, { ok: true });
|
|
2684
|
+
this.broadcastNotifSSE({ type: "cleared", unreadCount: 0 });
|
|
2685
|
+
}
|
|
2686
|
+
catch (err) {
|
|
2687
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
2688
|
+
}
|
|
2689
|
+
});
|
|
2690
|
+
}
|
|
2691
|
+
/** Badge-only: clear Client team-share UI metadata when Hub admin removes that memory. Does NOT touch chunks, embeddings, or hub_memories (recall paths). */
|
|
2692
|
+
handleSyncHubRemoval(req, res) {
|
|
2693
|
+
this.readBody(req, (body) => {
|
|
2694
|
+
try {
|
|
2695
|
+
const parsed = JSON.parse(body || "{}");
|
|
2696
|
+
const sourceChunkId = String(parsed.sourceChunkId || "");
|
|
2697
|
+
if (!sourceChunkId)
|
|
2698
|
+
return this.jsonResponse(res, { ok: false, error: "missing_source_chunk_id" }, 400);
|
|
2699
|
+
this.store.deleteTeamSharedChunk(sourceChunkId);
|
|
2700
|
+
this.jsonResponse(res, { ok: true, sourceChunkId });
|
|
2701
|
+
}
|
|
2702
|
+
catch (e) {
|
|
2703
|
+
this.jsonResponse(res, { ok: false, error: String(e) }, 500);
|
|
2704
|
+
}
|
|
2705
|
+
});
|
|
2706
|
+
}
|
|
2707
|
+
handleNotifSSE(req, res) {
|
|
2708
|
+
res.writeHead(200, {
|
|
2709
|
+
"Content-Type": "text/event-stream",
|
|
2710
|
+
"Cache-Control": "no-cache",
|
|
2711
|
+
Connection: "keep-alive",
|
|
2712
|
+
"Access-Control-Allow-Origin": "*",
|
|
2713
|
+
});
|
|
2714
|
+
res.write("data: {\"type\":\"connected\"}\n\n");
|
|
2715
|
+
this.notifSSEClients.push(res);
|
|
2716
|
+
if (!this.notifPollTimer)
|
|
2717
|
+
this.startNotifPoll();
|
|
2718
|
+
else
|
|
2719
|
+
this.notifPollImmediate();
|
|
2720
|
+
req.on("close", () => {
|
|
2721
|
+
this.notifSSEClients = this.notifSSEClients.filter((c) => c !== res);
|
|
2722
|
+
if (this.notifSSEClients.length === 0)
|
|
2723
|
+
this.stopNotifPoll();
|
|
2724
|
+
});
|
|
2725
|
+
}
|
|
2726
|
+
broadcastNotifSSE(data) {
|
|
2727
|
+
const msg = `data: ${JSON.stringify(data)}\n\n`;
|
|
2728
|
+
this.notifSSEClients = this.notifSSEClients.filter((c) => {
|
|
2729
|
+
try {
|
|
2730
|
+
c.write(msg);
|
|
2731
|
+
return true;
|
|
2732
|
+
}
|
|
2733
|
+
catch {
|
|
2734
|
+
return false;
|
|
2735
|
+
}
|
|
2736
|
+
});
|
|
2737
|
+
}
|
|
2738
|
+
startNotifPoll() {
|
|
2739
|
+
this.stopNotifPoll();
|
|
2740
|
+
const tick = async () => {
|
|
2741
|
+
const hub = this.resolveHubConnection();
|
|
2742
|
+
if (!hub)
|
|
2743
|
+
return;
|
|
2744
|
+
try {
|
|
2745
|
+
const data = (await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications?unread=1"));
|
|
2746
|
+
const count = data?.unreadCount ?? 0;
|
|
2747
|
+
if (count !== this.lastKnownNotifCount) {
|
|
2748
|
+
this.lastKnownNotifCount = count;
|
|
2749
|
+
this.broadcastNotifSSE({ type: "update", unreadCount: count });
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
catch { /* ignore */ }
|
|
2753
|
+
};
|
|
2754
|
+
tick();
|
|
2755
|
+
this.notifPollTimer = setInterval(tick, 3000);
|
|
2756
|
+
}
|
|
2757
|
+
stopNotifPoll() {
|
|
2758
|
+
if (this.notifPollTimer) {
|
|
2759
|
+
clearInterval(this.notifPollTimer);
|
|
2760
|
+
this.notifPollTimer = undefined;
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
notifPollImmediate() {
|
|
2764
|
+
const hub = this.resolveHubConnection();
|
|
2765
|
+
if (!hub)
|
|
2766
|
+
return;
|
|
2767
|
+
(0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications?unread=1")
|
|
2768
|
+
.then((data) => {
|
|
2769
|
+
const count = data?.unreadCount ?? 0;
|
|
2770
|
+
if (count !== this.lastKnownNotifCount) {
|
|
2771
|
+
this.lastKnownNotifCount = count;
|
|
2772
|
+
this.broadcastNotifSSE({ type: "update", unreadCount: count });
|
|
2773
|
+
}
|
|
2774
|
+
})
|
|
2775
|
+
.catch(() => { });
|
|
2776
|
+
}
|
|
2777
|
+
startHubHeartbeat() {
|
|
2778
|
+
this.stopHubHeartbeat();
|
|
2779
|
+
const sendHeartbeat = async () => {
|
|
2780
|
+
try {
|
|
2781
|
+
const hub = this.resolveHubConnection();
|
|
2782
|
+
if (!hub) {
|
|
2783
|
+
const persisted = this.store.getClientHubConnection();
|
|
2784
|
+
if (persisted?.hubUrl && persisted?.userToken) {
|
|
2785
|
+
await (0, hub_1.hubRequestJson)(persisted.hubUrl, persisted.userToken, "/api/v1/hub/heartbeat", { method: "POST" });
|
|
2786
|
+
}
|
|
2787
|
+
return;
|
|
2788
|
+
}
|
|
2789
|
+
await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/heartbeat", { method: "POST" });
|
|
2790
|
+
}
|
|
2791
|
+
catch { /* best-effort */ }
|
|
2792
|
+
};
|
|
2793
|
+
sendHeartbeat();
|
|
2794
|
+
this.hubHeartbeatTimer = setInterval(sendHeartbeat, ViewerServer.HUB_HEARTBEAT_INTERVAL_MS);
|
|
2795
|
+
}
|
|
2796
|
+
stopHubHeartbeat() {
|
|
2797
|
+
if (this.hubHeartbeatTimer) {
|
|
2798
|
+
clearInterval(this.hubHeartbeatTimer);
|
|
2799
|
+
this.hubHeartbeatTimer = undefined;
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2068
2802
|
getLocalIPs() {
|
|
2069
2803
|
const nets = node_os_1.default.networkInterfaces();
|
|
2070
2804
|
const ips = [];
|
|
@@ -2116,7 +2850,7 @@ class ViewerServer {
|
|
|
2116
2850
|
}
|
|
2117
2851
|
}
|
|
2118
2852
|
handleSaveConfig(req, res) {
|
|
2119
|
-
this.readBody(req, (body) => {
|
|
2853
|
+
this.readBody(req, async (body) => {
|
|
2120
2854
|
try {
|
|
2121
2855
|
const newCfg = JSON.parse(body);
|
|
2122
2856
|
const cfgPath = this.getOpenClawConfigPath();
|
|
@@ -2141,6 +2875,10 @@ class ViewerServer {
|
|
|
2141
2875
|
if (!entry.config)
|
|
2142
2876
|
entry.config = {};
|
|
2143
2877
|
const config = entry.config;
|
|
2878
|
+
const oldSharing = config.sharing;
|
|
2879
|
+
const oldSharingRole = oldSharing?.role;
|
|
2880
|
+
const oldSharingEnabled = Boolean(oldSharing?.enabled);
|
|
2881
|
+
const oldClientHubAddress = String(oldSharing?.client?.hubAddress || "");
|
|
2144
2882
|
if (newCfg.embedding)
|
|
2145
2883
|
config.embedding = newCfg.embedding;
|
|
2146
2884
|
if (newCfg.summarizer)
|
|
@@ -2160,12 +2898,14 @@ class ViewerServer {
|
|
|
2160
2898
|
if (merged.role === "client" && merged.client) {
|
|
2161
2899
|
const clientCfg = merged.client;
|
|
2162
2900
|
const addr = String(clientCfg.hubAddress || "");
|
|
2163
|
-
if (addr) {
|
|
2901
|
+
if (addr && oldSharingRole === "hub" && oldSharingEnabled) {
|
|
2902
|
+
const selfHubPort = oldSharing?.hub?.port ?? 18800;
|
|
2164
2903
|
const localIPs = this.getLocalIPs();
|
|
2165
2904
|
localIPs.push("127.0.0.1", "localhost", "0.0.0.0");
|
|
2166
2905
|
try {
|
|
2167
2906
|
const u = new URL(addr.startsWith("http") ? addr : `http://${addr}`);
|
|
2168
|
-
|
|
2907
|
+
const targetPort = u.port || (u.protocol === "https:" ? "443" : "80");
|
|
2908
|
+
if (localIPs.includes(u.hostname) && targetPort === String(selfHubPort)) {
|
|
2169
2909
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2170
2910
|
res.end(JSON.stringify({ error: "cannot_join_self" }));
|
|
2171
2911
|
return;
|
|
@@ -2174,18 +2914,70 @@ class ViewerServer {
|
|
|
2174
2914
|
catch { }
|
|
2175
2915
|
}
|
|
2176
2916
|
}
|
|
2917
|
+
const newRole = merged.role;
|
|
2918
|
+
const newEnabled = Boolean(merged.enabled);
|
|
2919
|
+
// Detect disabling sharing or switching away from hub mode
|
|
2920
|
+
const wasHub = oldSharingEnabled && oldSharingRole === "hub";
|
|
2921
|
+
const isHub = newEnabled && newRole === "hub";
|
|
2922
|
+
if (wasHub && !isHub) {
|
|
2923
|
+
await this.notifyHubShutdown();
|
|
2924
|
+
this.stopHubHeartbeat();
|
|
2925
|
+
this.log.info("Hub shutting down: notified connected clients");
|
|
2926
|
+
}
|
|
2927
|
+
// Detect disabling sharing or switching away from client mode
|
|
2928
|
+
const wasClient = oldSharingEnabled && oldSharingRole === "client";
|
|
2929
|
+
const isClient = newEnabled && newRole === "client";
|
|
2930
|
+
if (wasClient && !isClient) {
|
|
2931
|
+
await this.withdrawOrLeaveHub();
|
|
2932
|
+
this.store.clearClientHubConnection();
|
|
2933
|
+
this.log.info("Client hub connection cleared (sharing disabled or role changed)");
|
|
2934
|
+
}
|
|
2935
|
+
if (wasClient && isClient) {
|
|
2936
|
+
const newClientAddr = String(merged.client?.hubAddress || "");
|
|
2937
|
+
if (newClientAddr && oldClientHubAddress && (0, hub_1.normalizeHubUrl)(newClientAddr) !== (0, hub_1.normalizeHubUrl)(oldClientHubAddress)) {
|
|
2938
|
+
this.notifyHubLeave();
|
|
2939
|
+
const oldConn = this.store.getClientHubConnection();
|
|
2940
|
+
if (oldConn) {
|
|
2941
|
+
this.store.setClientHubConnection({ ...oldConn, hubUrl: (0, hub_1.normalizeHubUrl)(newClientAddr), userToken: "", lastKnownStatus: "hub_changed" });
|
|
2942
|
+
}
|
|
2943
|
+
this.log.info("Client hub connection token cleared (switched to different Hub), identity preserved");
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2177
2946
|
if (merged.role === "hub") {
|
|
2178
2947
|
merged.client = { hubAddress: "", userToken: "", teamToken: "" };
|
|
2179
2948
|
}
|
|
2180
2949
|
else if (merged.role === "client") {
|
|
2181
|
-
merged.hub = {
|
|
2950
|
+
merged.hub = { teamName: "", teamToken: "" };
|
|
2182
2951
|
}
|
|
2183
2952
|
config.sharing = merged;
|
|
2184
2953
|
}
|
|
2185
2954
|
node_fs_1.default.mkdirSync(node_path_1.default.dirname(cfgPath), { recursive: true });
|
|
2186
2955
|
node_fs_1.default.writeFileSync(cfgPath, JSON.stringify(raw, null, 2), "utf-8");
|
|
2187
2956
|
this.log.info("Plugin config updated via Viewer");
|
|
2188
|
-
this.
|
|
2957
|
+
this.stopHubHeartbeat();
|
|
2958
|
+
// When switching to client mode or re-enabling sharing as client, send join request
|
|
2959
|
+
const finalSharing = config.sharing;
|
|
2960
|
+
const nowClient = Boolean(finalSharing?.enabled) && finalSharing?.role === "client";
|
|
2961
|
+
const previouslyClient = oldSharingEnabled && oldSharingRole === "client";
|
|
2962
|
+
let joinStatus;
|
|
2963
|
+
if (nowClient && !previouslyClient) {
|
|
2964
|
+
try {
|
|
2965
|
+
joinStatus = await this.autoJoinOnSave(finalSharing);
|
|
2966
|
+
}
|
|
2967
|
+
catch (e) {
|
|
2968
|
+
this.log.warn(`Auto-join on save failed: ${e}`);
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
this.jsonResponse(res, { ok: true, joinStatus, restart: true });
|
|
2972
|
+
setTimeout(() => {
|
|
2973
|
+
this.log.info("config-save: triggering gateway restart via SIGUSR1...");
|
|
2974
|
+
try {
|
|
2975
|
+
process.kill(process.pid, "SIGUSR1");
|
|
2976
|
+
}
|
|
2977
|
+
catch (sig) {
|
|
2978
|
+
this.log.warn(`SIGUSR1 failed: ${sig}`);
|
|
2979
|
+
}
|
|
2980
|
+
}, 500);
|
|
2189
2981
|
}
|
|
2190
2982
|
catch (e) {
|
|
2191
2983
|
this.log.warn(`handleSaveConfig error: ${e}`);
|
|
@@ -2194,6 +2986,169 @@ class ViewerServer {
|
|
|
2194
2986
|
}
|
|
2195
2987
|
});
|
|
2196
2988
|
}
|
|
2989
|
+
async autoJoinOnSave(sharing) {
|
|
2990
|
+
const clientCfg = sharing.client;
|
|
2991
|
+
const hubAddress = String(clientCfg?.hubAddress || "");
|
|
2992
|
+
const teamToken = String(clientCfg?.teamToken || "");
|
|
2993
|
+
if (!hubAddress || !teamToken)
|
|
2994
|
+
return undefined;
|
|
2995
|
+
const hubUrl = (0, hub_1.normalizeHubUrl)(hubAddress);
|
|
2996
|
+
const os = await Promise.resolve().then(() => __importStar(require("os")));
|
|
2997
|
+
const nickname = String(clientCfg?.nickname || "");
|
|
2998
|
+
const username = nickname || os.userInfo().username || "user";
|
|
2999
|
+
const hostname = os.hostname() || "unknown";
|
|
3000
|
+
const persisted = this.store.getClientHubConnection();
|
|
3001
|
+
const existingIdentityKey = persisted?.identityKey || "";
|
|
3002
|
+
const result = await (0, hub_1.hubRequestJson)(hubUrl, "", "/api/v1/hub/join", {
|
|
3003
|
+
method: "POST",
|
|
3004
|
+
body: JSON.stringify({ teamToken, username, deviceName: hostname, identityKey: existingIdentityKey }),
|
|
3005
|
+
});
|
|
3006
|
+
const returnedIdentityKey = String(result.identityKey || existingIdentityKey || "");
|
|
3007
|
+
this.store.setClientHubConnection({
|
|
3008
|
+
hubUrl,
|
|
3009
|
+
userId: String(result.userId || ""),
|
|
3010
|
+
username,
|
|
3011
|
+
userToken: result.userToken || "",
|
|
3012
|
+
role: "member",
|
|
3013
|
+
connectedAt: Date.now(),
|
|
3014
|
+
identityKey: returnedIdentityKey,
|
|
3015
|
+
lastKnownStatus: result.status || "",
|
|
3016
|
+
});
|
|
3017
|
+
this.log.info(`Auto-join on save: status=${result.status}, userId=${result.userId}`);
|
|
3018
|
+
if (result.userToken) {
|
|
3019
|
+
this.startHubHeartbeat();
|
|
3020
|
+
}
|
|
3021
|
+
return result.status;
|
|
3022
|
+
}
|
|
3023
|
+
handleLeaveTeam(_req, res) {
|
|
3024
|
+
this.readBody(_req, async () => {
|
|
3025
|
+
try {
|
|
3026
|
+
await this.withdrawOrLeaveHub();
|
|
3027
|
+
this.store.clearClientHubConnection();
|
|
3028
|
+
const configPath = this.getOpenClawConfigPath();
|
|
3029
|
+
if (configPath && node_fs_1.default.existsSync(configPath)) {
|
|
3030
|
+
const raw = JSON.parse(node_fs_1.default.readFileSync(configPath, "utf8"));
|
|
3031
|
+
const pluginKey = Object.keys(raw.plugins?.entries ?? {}).find(k => k.includes("memos-local"));
|
|
3032
|
+
if (pluginKey) {
|
|
3033
|
+
const cfg = raw.plugins.entries[pluginKey].config ?? {};
|
|
3034
|
+
if (cfg.sharing) {
|
|
3035
|
+
cfg.sharing.enabled = false;
|
|
3036
|
+
cfg.sharing.client = { hubAddress: "", userToken: "", teamToken: "" };
|
|
3037
|
+
}
|
|
3038
|
+
raw.plugins.entries[pluginKey].config = cfg;
|
|
3039
|
+
node_fs_1.default.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
|
|
3040
|
+
this.log.info("handleLeaveTeam: config updated, sharing disabled");
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
this.jsonResponse(res, { ok: true, restart: true });
|
|
3044
|
+
setTimeout(() => {
|
|
3045
|
+
this.log.info("handleLeaveTeam: triggering gateway restart via SIGUSR1...");
|
|
3046
|
+
try {
|
|
3047
|
+
process.kill(process.pid, "SIGUSR1");
|
|
3048
|
+
}
|
|
3049
|
+
catch (sig) {
|
|
3050
|
+
this.log.warn(`SIGUSR1 failed: ${sig}`);
|
|
3051
|
+
}
|
|
3052
|
+
}, 500);
|
|
3053
|
+
}
|
|
3054
|
+
catch (e) {
|
|
3055
|
+
this.log.warn(`handleLeaveTeam error: ${e}`);
|
|
3056
|
+
this.jsonResponse(res, { ok: false, error: String(e) });
|
|
3057
|
+
}
|
|
3058
|
+
});
|
|
3059
|
+
}
|
|
3060
|
+
async notifyHubLeave() {
|
|
3061
|
+
try {
|
|
3062
|
+
const hub = this.resolveHubConnection();
|
|
3063
|
+
if (hub) {
|
|
3064
|
+
await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/leave", { method: "POST" });
|
|
3065
|
+
this.log.info("Notified Hub of voluntary leave");
|
|
3066
|
+
return;
|
|
3067
|
+
}
|
|
3068
|
+
const persisted = this.store.getClientHubConnection();
|
|
3069
|
+
if (persisted?.hubUrl && persisted?.userToken) {
|
|
3070
|
+
await (0, hub_1.hubRequestJson)(persisted.hubUrl, persisted.userToken, "/api/v1/hub/leave", { method: "POST" });
|
|
3071
|
+
this.log.info("Notified Hub of voluntary leave (persisted connection)");
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
catch (e) {
|
|
3075
|
+
this.log.warn(`Failed to notify Hub of leave: ${e}`);
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
async withdrawOrLeaveHub() {
|
|
3079
|
+
try {
|
|
3080
|
+
const persisted = this.store.getClientHubConnection();
|
|
3081
|
+
const sharing = this.ctx?.config?.sharing;
|
|
3082
|
+
if (persisted?.userToken && persisted?.hubUrl) {
|
|
3083
|
+
await (0, hub_1.hubRequestJson)(persisted.hubUrl, persisted.userToken, "/api/v1/hub/leave", { method: "POST" });
|
|
3084
|
+
this.log.info("Notified Hub of voluntary leave (had token)");
|
|
3085
|
+
return;
|
|
3086
|
+
}
|
|
3087
|
+
const hub = this.resolveHubConnection();
|
|
3088
|
+
if (hub?.userToken) {
|
|
3089
|
+
await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/leave", { method: "POST" });
|
|
3090
|
+
this.log.info("Notified Hub of voluntary leave (resolved connection)");
|
|
3091
|
+
return;
|
|
3092
|
+
}
|
|
3093
|
+
const hubUrl = persisted?.hubUrl || (sharing?.client?.hubAddress ? (0, hub_1.normalizeHubUrl)(sharing.client.hubAddress) : null);
|
|
3094
|
+
const userId = persisted?.userId;
|
|
3095
|
+
const teamToken = sharing?.client?.teamToken;
|
|
3096
|
+
if (hubUrl && userId && teamToken) {
|
|
3097
|
+
const withdrawUrl = `${(0, hub_1.normalizeHubUrl)(hubUrl)}/api/v1/hub/withdraw-pending`;
|
|
3098
|
+
await fetch(withdrawUrl, {
|
|
3099
|
+
method: "POST",
|
|
3100
|
+
headers: { "content-type": "application/json" },
|
|
3101
|
+
body: JSON.stringify({ teamToken, userId }),
|
|
3102
|
+
});
|
|
3103
|
+
this.log.info("Withdrew pending application from Hub");
|
|
3104
|
+
return;
|
|
3105
|
+
}
|
|
3106
|
+
this.log.info("No hub connection to clean up (no token, no pending)");
|
|
3107
|
+
}
|
|
3108
|
+
catch (e) {
|
|
3109
|
+
this.log.warn(`Failed to withdraw/leave Hub: ${e}`);
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
async notifyHubShutdown() {
|
|
3113
|
+
try {
|
|
3114
|
+
const sharing = this.ctx?.config.sharing;
|
|
3115
|
+
if (!sharing || sharing.role !== "hub")
|
|
3116
|
+
return;
|
|
3117
|
+
const hubPort = this.getHubPort();
|
|
3118
|
+
const authPath = node_path_1.default.join(this.dataDir, "hub-auth.json");
|
|
3119
|
+
let adminToken;
|
|
3120
|
+
try {
|
|
3121
|
+
const authData = JSON.parse(node_fs_1.default.readFileSync(authPath, "utf8"));
|
|
3122
|
+
adminToken = authData?.bootstrapAdminToken;
|
|
3123
|
+
}
|
|
3124
|
+
catch {
|
|
3125
|
+
return;
|
|
3126
|
+
}
|
|
3127
|
+
if (!adminToken)
|
|
3128
|
+
return;
|
|
3129
|
+
const users = this.store.listHubUsers("active");
|
|
3130
|
+
const { v4: uuidv4 } = require("uuid");
|
|
3131
|
+
for (const u of users) {
|
|
3132
|
+
try {
|
|
3133
|
+
this.store.insertHubNotification({
|
|
3134
|
+
id: uuidv4(),
|
|
3135
|
+
userId: u.id,
|
|
3136
|
+
type: "hub_shutdown",
|
|
3137
|
+
resource: "hub",
|
|
3138
|
+
title: "Hub is shutting down",
|
|
3139
|
+
message: "The Hub server is shutting down. You may be disconnected.",
|
|
3140
|
+
});
|
|
3141
|
+
}
|
|
3142
|
+
catch (e) {
|
|
3143
|
+
this.log.warn(`Failed to insert shutdown notification for user ${u.id}: ${e}`);
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
this.log.info(`Hub shutdown: notified ${users.length} approved user(s)`);
|
|
3147
|
+
}
|
|
3148
|
+
catch (e) {
|
|
3149
|
+
this.log.warn(`notifyHubShutdown error: ${e}`);
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
2197
3152
|
handleUpdateUsername(req, res) {
|
|
2198
3153
|
this.readBody(req, async (body) => {
|
|
2199
3154
|
if (!this.ctx)
|
|
@@ -2224,10 +3179,10 @@ class ViewerServer {
|
|
|
2224
3179
|
}
|
|
2225
3180
|
}
|
|
2226
3181
|
else {
|
|
2227
|
-
const
|
|
2228
|
-
if (
|
|
3182
|
+
const persistedConn = this.store.getClientHubConnection();
|
|
3183
|
+
if (persistedConn) {
|
|
2229
3184
|
this.store.setClientHubConnection({
|
|
2230
|
-
...
|
|
3185
|
+
...persistedConn,
|
|
2231
3186
|
username: result.username,
|
|
2232
3187
|
userToken: result.userToken,
|
|
2233
3188
|
});
|
|
@@ -2254,12 +3209,17 @@ class ViewerServer {
|
|
|
2254
3209
|
return;
|
|
2255
3210
|
}
|
|
2256
3211
|
try {
|
|
2257
|
-
const
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
3212
|
+
const sharing = this.ctx?.config?.sharing;
|
|
3213
|
+
if (sharing?.enabled && sharing.role === "hub") {
|
|
3214
|
+
const selfHubPort = this.getHubPort();
|
|
3215
|
+
const localIPs = this.getLocalIPs();
|
|
3216
|
+
localIPs.push("127.0.0.1", "localhost", "0.0.0.0");
|
|
3217
|
+
const parsed = new URL(hubUrl.startsWith("http") ? hubUrl : `http://${hubUrl}`);
|
|
3218
|
+
const targetPort = parsed.port || (parsed.protocol === "https:" ? "443" : "80");
|
|
3219
|
+
if (localIPs.includes(parsed.hostname) && targetPort === String(selfHubPort)) {
|
|
3220
|
+
this.jsonResponse(res, { ok: false, error: "cannot_join_self" });
|
|
3221
|
+
return;
|
|
3222
|
+
}
|
|
2263
3223
|
}
|
|
2264
3224
|
}
|
|
2265
3225
|
catch { }
|
|
@@ -2517,10 +3477,14 @@ class ViewerServer {
|
|
|
2517
3477
|
catch { }
|
|
2518
3478
|
this.log.info(`update-install: success! Updated to ${newVersion}`);
|
|
2519
3479
|
this.jsonResponse(res, { ok: true, version: newVersion });
|
|
2520
|
-
// Trigger Gateway restart after response is sent
|
|
2521
3480
|
setTimeout(() => {
|
|
2522
|
-
this.log.info(`update-install: triggering gateway restart...`);
|
|
2523
|
-
|
|
3481
|
+
this.log.info(`update-install: triggering gateway restart via SIGUSR1...`);
|
|
3482
|
+
try {
|
|
3483
|
+
process.kill(process.pid, "SIGUSR1");
|
|
3484
|
+
}
|
|
3485
|
+
catch (sig) {
|
|
3486
|
+
this.log.warn(`SIGUSR1 failed: ${sig}`);
|
|
3487
|
+
}
|
|
2524
3488
|
}, 500);
|
|
2525
3489
|
});
|
|
2526
3490
|
});
|
|
@@ -2668,7 +3632,7 @@ class ViewerServer {
|
|
|
2668
3632
|
// ─── Migration: scan OpenClaw built-in memory ───
|
|
2669
3633
|
getOpenClawHome() {
|
|
2670
3634
|
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
2671
|
-
return node_path_1.default.join(home, ".openclaw");
|
|
3635
|
+
return process.env.OPENCLAW_STATE_DIR || node_path_1.default.join(home, ".openclaw");
|
|
2672
3636
|
}
|
|
2673
3637
|
handleCleanupPolluted(res) {
|
|
2674
3638
|
try {
|
|
@@ -2696,7 +3660,7 @@ class ViewerServer {
|
|
|
2696
3660
|
try {
|
|
2697
3661
|
const ocHome = this.getOpenClawHome();
|
|
2698
3662
|
const memoryDir = node_path_1.default.join(ocHome, "memory");
|
|
2699
|
-
const
|
|
3663
|
+
const agentsDir = node_path_1.default.join(ocHome, "agents");
|
|
2700
3664
|
const sqliteFiles = [];
|
|
2701
3665
|
if (node_fs_1.default.existsSync(memoryDir)) {
|
|
2702
3666
|
for (const f of node_fs_1.default.readdirSync(memoryDir)) {
|
|
@@ -2714,38 +3678,45 @@ class ViewerServer {
|
|
|
2714
3678
|
}
|
|
2715
3679
|
let sessionCount = 0;
|
|
2716
3680
|
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
|
-
|
|
3681
|
+
if (node_fs_1.default.existsSync(agentsDir)) {
|
|
3682
|
+
for (const entry of node_fs_1.default.readdirSync(agentsDir, { withFileTypes: true })) {
|
|
3683
|
+
if (!entry.isDirectory())
|
|
3684
|
+
continue;
|
|
3685
|
+
const sessDir = node_path_1.default.join(agentsDir, entry.name, "sessions");
|
|
3686
|
+
if (!node_fs_1.default.existsSync(sessDir))
|
|
3687
|
+
continue;
|
|
3688
|
+
const jsonlFiles = node_fs_1.default.readdirSync(sessDir).filter(f => f.includes(".jsonl"));
|
|
3689
|
+
sessionCount += jsonlFiles.length;
|
|
3690
|
+
for (const f of jsonlFiles) {
|
|
3691
|
+
try {
|
|
3692
|
+
const content = node_fs_1.default.readFileSync(node_path_1.default.join(sessDir, f), "utf-8");
|
|
3693
|
+
const lines = content.split("\n").filter(l => l.trim());
|
|
3694
|
+
for (const line of lines) {
|
|
3695
|
+
try {
|
|
3696
|
+
const obj = JSON.parse(line);
|
|
3697
|
+
if (obj.type === "message") {
|
|
3698
|
+
const role = obj.message?.role ?? obj.role;
|
|
3699
|
+
if (role === "user" || role === "assistant") {
|
|
3700
|
+
const mc = obj.message?.content ?? obj.content;
|
|
3701
|
+
let txt = "";
|
|
3702
|
+
if (typeof mc === "string")
|
|
3703
|
+
txt = mc;
|
|
3704
|
+
else if (Array.isArray(mc))
|
|
3705
|
+
txt = mc.filter((p) => p.type === "text" && p.text).map((p) => p.text).join("\n");
|
|
3706
|
+
else
|
|
3707
|
+
txt = JSON.stringify(mc);
|
|
3708
|
+
if (role === "user")
|
|
3709
|
+
txt = (0, capture_1.stripInboundMetadata)(txt);
|
|
3710
|
+
if (txt && txt.length >= 10)
|
|
3711
|
+
messageCount++;
|
|
3712
|
+
}
|
|
2742
3713
|
}
|
|
2743
3714
|
}
|
|
3715
|
+
catch { /* skip bad lines */ }
|
|
2744
3716
|
}
|
|
2745
|
-
catch { /* skip bad lines */ }
|
|
2746
3717
|
}
|
|
3718
|
+
catch { /* skip unreadable */ }
|
|
2747
3719
|
}
|
|
2748
|
-
catch { /* skip unreadable */ }
|
|
2749
3720
|
}
|
|
2750
3721
|
}
|
|
2751
3722
|
const cfgPath = this.getOpenClawConfigPath();
|