@memtensor/memos-local-openclaw-plugin 1.0.4-beta.1 → 1.0.4-beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +7 -0
- package/README.md +24 -24
- package/dist/capture/index.d.ts +1 -1
- package/dist/capture/index.d.ts.map +1 -1
- package/dist/capture/index.js +34 -2
- package/dist/capture/index.js.map +1 -1
- package/dist/client/connector.d.ts +2 -2
- package/dist/client/connector.d.ts.map +1 -1
- package/dist/client/connector.js +122 -26
- package/dist/client/connector.js.map +1 -1
- package/dist/client/hub.d.ts.map +1 -1
- package/dist/client/hub.js +22 -0
- package/dist/client/hub.js.map +1 -1
- package/dist/client/skill-sync.d.ts +7 -0
- package/dist/client/skill-sync.d.ts.map +1 -1
- package/dist/client/skill-sync.js +10 -0
- package/dist/client/skill-sync.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +0 -2
- package/dist/config.js.map +1 -1
- package/dist/hub/server.d.ts +8 -0
- package/dist/hub/server.d.ts.map +1 -1
- package/dist/hub/server.js +390 -106
- package/dist/hub/server.js.map +1 -1
- package/dist/hub/user-manager.d.ts +11 -0
- package/dist/hub/user-manager.d.ts.map +1 -1
- package/dist/hub/user-manager.js +31 -3
- package/dist/hub/user-manager.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -2
- package/dist/index.js.map +1 -1
- package/dist/ingest/providers/index.d.ts.map +1 -1
- package/dist/ingest/providers/index.js +37 -6
- package/dist/ingest/providers/index.js.map +1 -1
- package/dist/recall/engine.d.ts.map +1 -1
- package/dist/recall/engine.js +93 -1
- package/dist/recall/engine.js.map +1 -1
- package/dist/shared/llm-call.d.ts +1 -0
- package/dist/shared/llm-call.d.ts.map +1 -1
- package/dist/shared/llm-call.js +82 -8
- package/dist/shared/llm-call.js.map +1 -1
- package/dist/sharing/types.d.ts +1 -1
- package/dist/sharing/types.d.ts.map +1 -1
- package/dist/skill/evolver.d.ts +4 -0
- package/dist/skill/evolver.d.ts.map +1 -1
- package/dist/skill/evolver.js +59 -5
- package/dist/skill/evolver.js.map +1 -1
- package/dist/skill/generator.d.ts +2 -0
- package/dist/skill/generator.d.ts.map +1 -1
- package/dist/skill/generator.js +45 -3
- package/dist/skill/generator.js.map +1 -1
- package/dist/skill/installer.d.ts +26 -0
- package/dist/skill/installer.d.ts.map +1 -1
- package/dist/skill/installer.js +80 -4
- package/dist/skill/installer.js.map +1 -1
- package/dist/skill/upgrader.d.ts +2 -0
- package/dist/skill/upgrader.d.ts.map +1 -1
- package/dist/skill/upgrader.js +139 -1
- package/dist/skill/upgrader.js.map +1 -1
- package/dist/skill/validator.d.ts +3 -0
- package/dist/skill/validator.d.ts.map +1 -1
- package/dist/skill/validator.js +75 -0
- package/dist/skill/validator.js.map +1 -1
- package/dist/storage/ensure-binding.d.ts +12 -0
- package/dist/storage/ensure-binding.d.ts.map +1 -0
- package/dist/storage/ensure-binding.js +53 -0
- package/dist/storage/ensure-binding.js.map +1 -0
- package/dist/storage/sqlite.d.ts +89 -20
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +374 -124
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/telemetry.d.ts +12 -5
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +156 -40
- package/dist/telemetry.js.map +1 -1
- package/dist/tools/memory-search.d.ts +3 -1
- package/dist/tools/memory-search.d.ts.map +1 -1
- package/dist/tools/memory-search.js +3 -1
- package/dist/tools/memory-search.js.map +1 -1
- package/dist/types.d.ts +11 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -1
- package/dist/viewer/html.d.ts.map +1 -1
- package/dist/viewer/html.js +2671 -879
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +30 -8
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +990 -198
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +700 -56
- package/openclaw.plugin.json +1 -1
- package/package.json +3 -2
- package/scripts/postinstall.cjs +1 -1
- package/skill/memos-memory-guide/SKILL.md +64 -26
- package/src/capture/index.ts +37 -1
- package/src/client/connector.ts +124 -28
- package/src/client/hub.ts +18 -0
- package/src/client/skill-sync.ts +14 -0
- package/src/config.ts +0 -2
- package/src/hub/server.ts +374 -97
- package/src/hub/user-manager.ts +48 -8
- package/src/index.ts +10 -2
- package/src/ingest/providers/index.ts +41 -7
- package/src/recall/engine.ts +86 -1
- package/src/shared/llm-call.ts +97 -9
- package/src/sharing/types.ts +1 -1
- package/src/skill/evolver.ts +63 -6
- package/src/skill/generator.ts +44 -5
- package/src/skill/installer.ts +107 -4
- package/src/skill/upgrader.ts +139 -1
- package/src/skill/validator.ts +79 -0
- package/src/storage/ensure-binding.ts +52 -0
- package/src/storage/sqlite.ts +395 -148
- package/src/telemetry.ts +172 -41
- package/src/tools/memory-search.ts +2 -1
- package/src/types.ts +12 -2
- package/src/viewer/html.ts +2671 -879
- package/src/viewer/server.ts +913 -182
package/src/viewer/server.ts
CHANGED
|
@@ -84,6 +84,12 @@ export class ViewerServer {
|
|
|
84
84
|
{ running: false, done: false, stopped: false, processed: 0, total: 0, tasksCreated: 0, skillsCreated: 0, errors: 0, skippedSessions: 0, totalSessions: 0 };
|
|
85
85
|
private ppSSEClients: http.ServerResponse[] = [];
|
|
86
86
|
|
|
87
|
+
private notifSSEClients: http.ServerResponse[] = [];
|
|
88
|
+
private notifPollTimer?: ReturnType<typeof setInterval>;
|
|
89
|
+
private lastKnownNotifCount = 0;
|
|
90
|
+
private hubHeartbeatTimer?: ReturnType<typeof setInterval>;
|
|
91
|
+
private static readonly HUB_HEARTBEAT_INTERVAL_MS = 45_000;
|
|
92
|
+
|
|
87
93
|
constructor(opts: ViewerServerOptions) {
|
|
88
94
|
this.store = opts.store;
|
|
89
95
|
this.embedder = opts.embedder;
|
|
@@ -103,15 +109,16 @@ export class ViewerServer {
|
|
|
103
109
|
this.server.on("error", (err: NodeJS.ErrnoException) => {
|
|
104
110
|
if (err.code === "EADDRINUSE") {
|
|
105
111
|
this.log.warn(`Viewer port ${this.port} in use, trying ${this.port + 1}`);
|
|
106
|
-
this.server!.listen(this.port + 1, "
|
|
112
|
+
this.server!.listen(this.port + 1, "0.0.0.0");
|
|
107
113
|
} else {
|
|
108
114
|
reject(err);
|
|
109
115
|
}
|
|
110
116
|
});
|
|
111
|
-
this.server.listen(this.port, "
|
|
117
|
+
this.server.listen(this.port, "0.0.0.0", () => {
|
|
112
118
|
const addr = this.server!.address();
|
|
113
119
|
const actualPort = typeof addr === "object" && addr ? addr.port : this.port;
|
|
114
120
|
this.autoCleanupPolluted();
|
|
121
|
+
this.startHubHeartbeat();
|
|
115
122
|
resolve(`http://127.0.0.1:${actualPort}`);
|
|
116
123
|
});
|
|
117
124
|
});
|
|
@@ -134,6 +141,10 @@ export class ViewerServer {
|
|
|
134
141
|
}
|
|
135
142
|
|
|
136
143
|
stop(): void {
|
|
144
|
+
this.stopHubHeartbeat();
|
|
145
|
+
this.stopNotifPoll();
|
|
146
|
+
for (const c of this.notifSSEClients) { try { c.end(); } catch {} }
|
|
147
|
+
this.notifSSEClients = [];
|
|
137
148
|
this.server?.close();
|
|
138
149
|
this.server = null;
|
|
139
150
|
}
|
|
@@ -224,6 +235,11 @@ export class ViewerServer {
|
|
|
224
235
|
}
|
|
225
236
|
|
|
226
237
|
if (p === "/api/memories" && req.method === "GET") this.serveMemories(res, url);
|
|
238
|
+
else if (p === "/api/memories/share-local" && req.method === "POST") this.handleMemoryLocalShare(req, res);
|
|
239
|
+
else if (p === "/api/memories/unshare-local" && req.method === "POST") this.handleMemoryLocalUnshare(req, res);
|
|
240
|
+
else if (p.match(/^\/api\/memory\/[^/]+\/scope$/) && req.method === "PUT") this.handleMemoryScope(req, res, p);
|
|
241
|
+
else if (p.match(/^\/api\/task\/[^/]+\/scope$/) && req.method === "PUT") this.handleTaskScope(req, res, p);
|
|
242
|
+
else if (p.match(/^\/api\/skill\/[^/]+\/scope$/) && req.method === "PUT") this.handleSkillScope(req, res, p);
|
|
227
243
|
else if (p === "/api/stats") this.serveStats(res, url);
|
|
228
244
|
else if (p === "/api/metrics") this.serveMetrics(res, url);
|
|
229
245
|
else if (p === "/api/tool-metrics") this.serveToolMetrics(res, url);
|
|
@@ -251,6 +267,8 @@ export class ViewerServer {
|
|
|
251
267
|
else if (p === "/api/sharing/pending-users" && req.method === "GET") this.serveSharingPendingUsers(res);
|
|
252
268
|
else if (p === "/api/sharing/approve-user" && req.method === "POST") this.handleSharingApproveUser(req, res);
|
|
253
269
|
else if (p === "/api/sharing/reject-user" && req.method === "POST") this.handleSharingRejectUser(req, res);
|
|
270
|
+
else if (p === "/api/sharing/remove-user" && req.method === "POST") this.handleSharingRemoveUser(req, res);
|
|
271
|
+
else if (p === "/api/sharing/change-role" && req.method === "POST") this.handleSharingChangeRole(req, res);
|
|
254
272
|
else if (p === "/api/sharing/retry-join" && req.method === "POST") this.handleRetryJoin(req, res);
|
|
255
273
|
else if (p === "/api/sharing/search/memories" && req.method === "POST") this.handleSharingMemorySearch(req, res);
|
|
256
274
|
else if (p === "/api/sharing/memories/list" && req.method === "GET") this.serveSharingMemoryList(res, url);
|
|
@@ -261,23 +279,23 @@ export class ViewerServer {
|
|
|
261
279
|
else if (p === "/api/sharing/tasks/share" && req.method === "POST") this.handleSharingTaskShare(req, res);
|
|
262
280
|
else if (p === "/api/sharing/tasks/unshare" && req.method === "POST") this.handleSharingTaskUnshare(req, res);
|
|
263
281
|
else if (p === "/api/sharing/update-username" && req.method === "POST") this.handleUpdateUsername(req, res);
|
|
282
|
+
else if (p === "/api/sharing/rename-user" && req.method === "POST") this.handleAdminRenameUser(req, res);
|
|
264
283
|
else if (p === "/api/sharing/test-hub" && req.method === "POST") this.handleTestHubConnection(req, res);
|
|
265
284
|
else if (p === "/api/sharing/memories/share" && req.method === "POST") this.handleSharingMemoryShare(req, res);
|
|
266
285
|
else if (p === "/api/sharing/memories/unshare" && req.method === "POST") this.handleSharingMemoryUnshare(req, res);
|
|
267
286
|
else if (p === "/api/sharing/skills/pull" && req.method === "POST") this.handleSharingSkillPull(req, res);
|
|
268
287
|
else if (p === "/api/sharing/skills/share" && req.method === "POST") this.handleSharingSkillShare(req, res);
|
|
269
288
|
else if (p === "/api/sharing/skills/unshare" && req.method === "POST") this.handleSharingSkillUnshare(req, res);
|
|
270
|
-
else if (p === "/api/sharing/groups" && req.method === "GET") this.serveSharingGroups(res);
|
|
271
|
-
else if (p === "/api/sharing/groups" && req.method === "POST") this.handleSharingGroupCreate(req, res);
|
|
272
|
-
else if (p.match(/^\/api\/sharing\/groups\/[^/]+$/) && req.method === "PUT") this.handleSharingGroupUpdate(req, res, p);
|
|
273
|
-
else if (p.match(/^\/api\/sharing\/groups\/[^/]+$/) && req.method === "DELETE") this.handleSharingGroupDelete(res, p);
|
|
274
|
-
else if (p.match(/^\/api\/sharing\/groups\/[^/]+\/members$/) && req.method === "GET") this.serveSharingGroupMembers(res, p);
|
|
275
|
-
else if (p.match(/^\/api\/sharing\/groups\/[^/]+\/members$/) && req.method === "POST") this.handleSharingGroupAddMember(req, res, p);
|
|
276
|
-
else if (p.match(/^\/api\/sharing\/groups\/[^/]+\/members$/) && req.method === "DELETE") this.handleSharingGroupRemoveMember(req, res, p);
|
|
277
289
|
else if (p === "/api/sharing/users" && req.method === "GET") this.serveSharingUsers(res);
|
|
290
|
+
else if (p === "/api/sharing/notifications" && req.method === "GET") this.serveSharingNotifications(res, url);
|
|
291
|
+
else if (p === "/api/sharing/notifications/read" && req.method === "POST") this.handleSharingNotificationsRead(req, res);
|
|
292
|
+
else if (p === "/api/sharing/notifications/clear" && req.method === "POST") this.handleSharingNotificationsClear(req, res);
|
|
293
|
+
else if (p === "/api/notifications/stream" && req.method === "GET") this.handleNotifSSE(req, res);
|
|
278
294
|
else if (p === "/api/admin/shared-tasks" && req.method === "GET") this.serveAdminSharedTasks(res);
|
|
295
|
+
else if (p.match(/^\/api\/admin\/shared-tasks\/[^/]+\/detail$/) && req.method === "GET") this.serveHubTaskDetail(res, p);
|
|
279
296
|
else if (p.match(/^\/api\/admin\/shared-tasks\/[^/]+$/) && req.method === "DELETE") this.handleAdminDeleteTask(res, p);
|
|
280
297
|
else if (p === "/api/admin/shared-skills" && req.method === "GET") this.serveAdminSharedSkills(res);
|
|
298
|
+
else if (p.match(/^\/api\/admin\/shared-skills\/[^/]+\/detail$/) && req.method === "GET") this.serveHubSkillDetail(res, p);
|
|
281
299
|
else if (p.match(/^\/api\/admin\/shared-skills\/[^/]+$/) && req.method === "DELETE") this.handleAdminDeleteSkill(res, p);
|
|
282
300
|
else if (p === "/api/admin/shared-memories" && req.method === "GET") this.serveAdminSharedMemories(res);
|
|
283
301
|
else if (p.match(/^\/api\/admin\/shared-memories\/[^/]+$/) && req.method === "DELETE") this.handleAdminDeleteMemory(res, p);
|
|
@@ -432,22 +450,30 @@ export class ViewerServer {
|
|
|
432
450
|
const params: any[] = [];
|
|
433
451
|
if (session) { conditions.push("session_key = ?"); params.push(session); }
|
|
434
452
|
if (role) { conditions.push("role = ?"); params.push(role); }
|
|
435
|
-
if (owner
|
|
453
|
+
if (owner && owner.startsWith("agent:")) {
|
|
454
|
+
conditions.push("(owner = ? OR owner = 'public')");
|
|
455
|
+
params.push(owner);
|
|
456
|
+
} else if (owner) {
|
|
457
|
+
conditions.push("owner = ?"); params.push(owner);
|
|
458
|
+
}
|
|
436
459
|
if (dateFrom) { conditions.push("created_at >= ?"); params.push(new Date(dateFrom).getTime()); }
|
|
437
460
|
if (dateTo) { conditions.push("created_at <= ?"); params.push(new Date(dateTo).getTime()); }
|
|
438
461
|
|
|
439
462
|
const where = conditions.length > 0 ? " WHERE " + conditions.join(" AND ") : "";
|
|
440
463
|
const totalRow = db.prepare("SELECT COUNT(*) as count FROM chunks" + where).get(...params) as any;
|
|
441
|
-
const rawMemories = db.prepare("SELECT * FROM chunks" + where + ` ORDER BY created_at ${sortBy} LIMIT ? OFFSET ?`).all(...params, limit, offset);
|
|
464
|
+
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);
|
|
442
465
|
const findMergeSources = db.prepare("SELECT id, summary, role FROM chunks WHERE dedup_target = ? AND (dedup_status = 'merged' OR dedup_status = 'duplicate')");
|
|
443
466
|
|
|
444
467
|
const chunkIds = rawMemories.map((m: any) => m.id);
|
|
445
468
|
const sharingMap = new Map<string, { visibility: string; group_id: string | null }>();
|
|
469
|
+
const localShareMap = new Map<string, { original_owner: string; shared_at: number }>();
|
|
446
470
|
if (chunkIds.length > 0) {
|
|
447
471
|
try {
|
|
448
472
|
const placeholders = chunkIds.map(() => "?").join(",");
|
|
449
473
|
const sharedRows = db.prepare(`SELECT source_chunk_id, visibility, group_id FROM hub_memories WHERE source_chunk_id IN (${placeholders})`).all(...chunkIds) as Array<{ source_chunk_id: string; visibility: string; group_id: string | null }>;
|
|
450
474
|
for (const r of sharedRows) sharingMap.set(r.source_chunk_id, r);
|
|
475
|
+
const localRows = db.prepare(`SELECT chunk_id, original_owner, shared_at FROM local_shared_memories WHERE chunk_id IN (${placeholders})`).all(...chunkIds) as Array<{ chunk_id: string; original_owner: string; shared_at: number }>;
|
|
476
|
+
for (const r of localRows) localShareMap.set(r.chunk_id, r);
|
|
451
477
|
} catch {
|
|
452
478
|
}
|
|
453
479
|
}
|
|
@@ -458,8 +484,12 @@ export class ViewerServer {
|
|
|
458
484
|
out.merge_sources = sources;
|
|
459
485
|
}
|
|
460
486
|
const shared = sharingMap.get(m.id);
|
|
487
|
+
const localShared = localShareMap.get(m.id);
|
|
461
488
|
out.sharingVisibility = shared?.visibility ?? null;
|
|
462
489
|
out.sharingGroupId = shared?.group_id ?? null;
|
|
490
|
+
out.localSharing = out.owner === "public";
|
|
491
|
+
out.localSharingManaged = !!localShared;
|
|
492
|
+
out.localOriginalOwner = localShared?.original_owner ?? null;
|
|
463
493
|
return out;
|
|
464
494
|
});
|
|
465
495
|
|
|
@@ -477,7 +507,21 @@ export class ViewerServer {
|
|
|
477
507
|
}
|
|
478
508
|
|
|
479
509
|
private serveToolMetrics(res: http.ServerResponse, url: URL): void {
|
|
480
|
-
const
|
|
510
|
+
const fromParam = url.searchParams.get("from");
|
|
511
|
+
const toParam = url.searchParams.get("to");
|
|
512
|
+
if (fromParam) {
|
|
513
|
+
const fromMs = new Date(fromParam).getTime();
|
|
514
|
+
const toMs = toParam ? new Date(toParam).getTime() : Date.now();
|
|
515
|
+
if (isNaN(fromMs) || isNaN(toMs)) {
|
|
516
|
+
this.jsonResponse(res, { error: "Invalid date" }, 400);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
const diffMin = Math.max(10, Math.min(43200, Math.round((toMs - fromMs) / 60000)));
|
|
520
|
+
const data = this.store.getToolMetrics(diffMin, fromMs, toMs);
|
|
521
|
+
this.jsonResponse(res, data);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
const minutes = Math.min(43200, Math.max(10, Number(url.searchParams.get("minutes")) || 60));
|
|
481
525
|
const data = this.store.getToolMetrics(minutes);
|
|
482
526
|
this.jsonResponse(res, data);
|
|
483
527
|
}
|
|
@@ -485,13 +529,15 @@ export class ViewerServer {
|
|
|
485
529
|
private serveTasks(res: http.ServerResponse, url: URL): void {
|
|
486
530
|
this.store.recordViewerEvent("tasks_list");
|
|
487
531
|
const status = url.searchParams.get("status") ?? undefined;
|
|
532
|
+
const owner = url.searchParams.get("owner") ?? undefined;
|
|
488
533
|
const limit = Math.min(100, Math.max(1, Number(url.searchParams.get("limit")) || 50));
|
|
489
534
|
const offset = Math.max(0, Number(url.searchParams.get("offset")) || 0);
|
|
490
|
-
const { tasks, total } = this.store.listTasks({ status, limit, offset });
|
|
535
|
+
const { tasks, total } = this.store.listTasks({ status, limit, offset, owner });
|
|
491
536
|
|
|
492
537
|
const db = (this.store as any).db;
|
|
493
538
|
const items = tasks.map((t) => {
|
|
494
|
-
const meta = db.prepare("SELECT skill_status FROM tasks WHERE id = ?").get(t.id) as { skill_status: string | null } | undefined;
|
|
539
|
+
const meta = db.prepare("SELECT skill_status, owner FROM tasks WHERE id = ?").get(t.id) as { skill_status: string | null; owner: string | null } | undefined;
|
|
540
|
+
const sharedTask = db.prepare("SELECT visibility FROM hub_tasks WHERE source_task_id = ? ORDER BY updated_at DESC LIMIT 1").get(t.id) as { visibility: string } | undefined;
|
|
495
541
|
return {
|
|
496
542
|
id: t.id,
|
|
497
543
|
sessionKey: t.sessionKey,
|
|
@@ -502,6 +548,8 @@ export class ViewerServer {
|
|
|
502
548
|
endedAt: t.endedAt,
|
|
503
549
|
chunkCount: this.store.countChunksByTask(t.id),
|
|
504
550
|
skillStatus: meta?.skill_status ?? null,
|
|
551
|
+
owner: meta?.owner ?? "agent:main",
|
|
552
|
+
sharingVisibility: sharedTask?.visibility ?? null,
|
|
505
553
|
};
|
|
506
554
|
});
|
|
507
555
|
|
|
@@ -544,6 +592,7 @@ export class ViewerServer {
|
|
|
544
592
|
title: task.title,
|
|
545
593
|
summary: task.summary,
|
|
546
594
|
status: task.status,
|
|
595
|
+
owner: task.owner ?? "agent:main",
|
|
547
596
|
startedAt: task.startedAt,
|
|
548
597
|
endedAt: task.endedAt,
|
|
549
598
|
chunks: chunkItems,
|
|
@@ -552,6 +601,7 @@ export class ViewerServer {
|
|
|
552
601
|
skillLinks,
|
|
553
602
|
sharingVisibility: sharedTask?.visibility ?? null,
|
|
554
603
|
sharingGroupId: sharedTask?.group_id ?? null,
|
|
604
|
+
hubTaskId: sharedTask ? true : false,
|
|
555
605
|
});
|
|
556
606
|
}
|
|
557
607
|
|
|
@@ -586,12 +636,19 @@ export class ViewerServer {
|
|
|
586
636
|
}
|
|
587
637
|
let embCount = 0;
|
|
588
638
|
try { embCount = (db.prepare("SELECT COUNT(*) as count FROM embeddings").get() as any).count; } catch { /* table may not exist */ }
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
639
|
+
let sessionQuery: string;
|
|
640
|
+
let sessionParams: any[];
|
|
641
|
+
if (ownerFilter && ownerFilter.startsWith("agent:")) {
|
|
642
|
+
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";
|
|
643
|
+
sessionParams = [ownerFilter];
|
|
644
|
+
} else if (ownerFilter) {
|
|
645
|
+
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";
|
|
646
|
+
sessionParams = [ownerFilter];
|
|
647
|
+
} else {
|
|
648
|
+
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";
|
|
649
|
+
sessionParams = [];
|
|
650
|
+
}
|
|
651
|
+
const sessionList = db.prepare(sessionQuery).all(...sessionParams) as any[];
|
|
595
652
|
|
|
596
653
|
let skillCount = 0;
|
|
597
654
|
try { skillCount = (db.prepare("SELECT COUNT(*) as count FROM skills").get() as any).count; } catch { /* table may not exist yet */ }
|
|
@@ -604,10 +661,16 @@ export class ViewerServer {
|
|
|
604
661
|
|
|
605
662
|
let owners: string[] = [];
|
|
606
663
|
try {
|
|
607
|
-
const ownerRows = db.prepare("SELECT DISTINCT owner FROM chunks WHERE owner IS NOT NULL ORDER BY owner").all() as any[];
|
|
664
|
+
const ownerRows = db.prepare("SELECT DISTINCT owner FROM chunks WHERE owner IS NOT NULL AND owner LIKE 'agent:%' ORDER BY owner").all() as any[];
|
|
608
665
|
owners = ownerRows.map((o: any) => o.owner);
|
|
609
666
|
} catch { /* column may not exist yet */ }
|
|
610
667
|
|
|
668
|
+
let currentAgentOwner = "agent:main";
|
|
669
|
+
try {
|
|
670
|
+
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() as any;
|
|
671
|
+
if (latest?.owner) currentAgentOwner = latest.owner;
|
|
672
|
+
} catch { /* best-effort */ }
|
|
673
|
+
|
|
611
674
|
this.jsonResponse(res, {
|
|
612
675
|
totalMemories: total.count, totalSessions: sessions.count, totalEmbeddings: embCount,
|
|
613
676
|
totalSkills: skillCount,
|
|
@@ -616,6 +679,7 @@ export class ViewerServer {
|
|
|
616
679
|
timeRange: { earliest: timeRange.earliest, latest: timeRange.latest },
|
|
617
680
|
sessions: sessionList,
|
|
618
681
|
owners,
|
|
682
|
+
currentAgentOwner,
|
|
619
683
|
});
|
|
620
684
|
} catch (e) {
|
|
621
685
|
this.log.warn(`stats error: ${e}`);
|
|
@@ -727,7 +791,12 @@ export class ViewerServer {
|
|
|
727
791
|
if (visibility) {
|
|
728
792
|
skills = skills.filter(s => s.visibility === visibility);
|
|
729
793
|
}
|
|
730
|
-
this.
|
|
794
|
+
const db = (this.store as any).db;
|
|
795
|
+
const enriched = skills.map(s => {
|
|
796
|
+
const hub = db.prepare("SELECT visibility FROM hub_skills WHERE source_skill_id = ? ORDER BY updated_at DESC LIMIT 1").get(s.id) as { visibility: string } | undefined;
|
|
797
|
+
return { ...s, sharingVisibility: hub?.visibility ?? null };
|
|
798
|
+
});
|
|
799
|
+
this.jsonResponse(res, { skills: enriched });
|
|
731
800
|
}
|
|
732
801
|
|
|
733
802
|
private serveSkillDetail(res: http.ServerResponse, urlPath: string): void {
|
|
@@ -978,11 +1047,21 @@ export class ViewerServer {
|
|
|
978
1047
|
});
|
|
979
1048
|
}
|
|
980
1049
|
|
|
981
|
-
private handleSkillDelete(res: http.ServerResponse, urlPath: string): void {
|
|
1050
|
+
private async handleSkillDelete(res: http.ServerResponse, urlPath: string): Promise<void> {
|
|
982
1051
|
const skillId = urlPath.replace("/api/skill/", "");
|
|
983
1052
|
const skill = this.store.getSkill(skillId);
|
|
984
1053
|
if (!skill) { res.writeHead(404, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Skill not found" })); return; }
|
|
985
|
-
|
|
1054
|
+
try {
|
|
1055
|
+
const hub = this.resolveHubConnection();
|
|
1056
|
+
if (hub) {
|
|
1057
|
+
await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/skills/unpublish", {
|
|
1058
|
+
method: "POST",
|
|
1059
|
+
body: JSON.stringify({ sourceSkillId: skillId }),
|
|
1060
|
+
}).catch(() => {});
|
|
1061
|
+
}
|
|
1062
|
+
const db = (this.store as any).db;
|
|
1063
|
+
db.prepare("DELETE FROM hub_skills WHERE source_skill_id = ?").run(skillId);
|
|
1064
|
+
} catch (_) {}
|
|
986
1065
|
try {
|
|
987
1066
|
if (skill.dirPath && fs.existsSync(skill.dirPath)) {
|
|
988
1067
|
fs.rmSync(skill.dirPath, { recursive: true, force: true });
|
|
@@ -1029,7 +1108,15 @@ export class ViewerServer {
|
|
|
1029
1108
|
const cleaned = chunk.role === "user" && chunk.content
|
|
1030
1109
|
? { ...chunk, content: stripInboundMetadata(chunk.content) }
|
|
1031
1110
|
: chunk;
|
|
1032
|
-
this.
|
|
1111
|
+
const localShared = this.store.getLocalSharedMemory(chunkId);
|
|
1112
|
+
this.jsonResponse(res, {
|
|
1113
|
+
memory: {
|
|
1114
|
+
...cleaned,
|
|
1115
|
+
localSharing: cleaned.owner === "public",
|
|
1116
|
+
localSharingManaged: !!localShared,
|
|
1117
|
+
localOriginalOwner: localShared?.originalOwner ?? null,
|
|
1118
|
+
},
|
|
1119
|
+
});
|
|
1033
1120
|
}
|
|
1034
1121
|
|
|
1035
1122
|
private handleUpdate(req: http.IncomingMessage, res: http.ServerResponse, urlPath: string): void {
|
|
@@ -1058,6 +1145,349 @@ export class ViewerServer {
|
|
|
1058
1145
|
else { res.writeHead(404, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Not found" })); }
|
|
1059
1146
|
}
|
|
1060
1147
|
|
|
1148
|
+
private handleMemoryLocalShare(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
1149
|
+
this.readBody(req, (body) => {
|
|
1150
|
+
try {
|
|
1151
|
+
const parsed = JSON.parse(body || "{}");
|
|
1152
|
+
const chunkId = String(parsed.chunkId || "");
|
|
1153
|
+
if (!chunkId) return this.jsonResponse(res, { ok: false, error: "missing_chunk_id" }, 400);
|
|
1154
|
+
const result = this.store.markMemorySharedLocally(chunkId);
|
|
1155
|
+
if (!result.ok) {
|
|
1156
|
+
return this.jsonResponse(res, { ok: false, error: result.reason ?? "share_failed" }, result.reason === "not_found" ? 404 : 400);
|
|
1157
|
+
}
|
|
1158
|
+
this.jsonResponse(res, {
|
|
1159
|
+
ok: true,
|
|
1160
|
+
chunkId,
|
|
1161
|
+
owner: result.owner,
|
|
1162
|
+
localSharing: true,
|
|
1163
|
+
localSharingManaged: true,
|
|
1164
|
+
localOriginalOwner: result.originalOwner ?? null,
|
|
1165
|
+
});
|
|
1166
|
+
} catch (err) {
|
|
1167
|
+
this.jsonResponse(res, { ok: false, error: String(err) }, 400);
|
|
1168
|
+
}
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
private handleMemoryLocalUnshare(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
1173
|
+
this.readBody(req, (body) => {
|
|
1174
|
+
try {
|
|
1175
|
+
const parsed = JSON.parse(body || "{}");
|
|
1176
|
+
const chunkId = String(parsed.chunkId || "");
|
|
1177
|
+
const privateOwner = typeof parsed.privateOwner === "string" ? parsed.privateOwner : undefined;
|
|
1178
|
+
if (!chunkId) return this.jsonResponse(res, { ok: false, error: "missing_chunk_id" }, 400);
|
|
1179
|
+
const result = this.store.unmarkMemorySharedLocally(chunkId, privateOwner);
|
|
1180
|
+
if (!result.ok) {
|
|
1181
|
+
return this.jsonResponse(res, { ok: false, error: result.reason ?? "unshare_failed" }, result.reason === "not_found" ? 404 : 400);
|
|
1182
|
+
}
|
|
1183
|
+
this.jsonResponse(res, {
|
|
1184
|
+
ok: true,
|
|
1185
|
+
chunkId,
|
|
1186
|
+
owner: result.owner,
|
|
1187
|
+
localSharing: false,
|
|
1188
|
+
localOriginalOwner: result.originalOwner ?? null,
|
|
1189
|
+
});
|
|
1190
|
+
} catch (err) {
|
|
1191
|
+
this.jsonResponse(res, { ok: false, error: String(err) }, 400);
|
|
1192
|
+
}
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// ─── Unified scope API ───
|
|
1197
|
+
|
|
1198
|
+
private handleMemoryScope(req: http.IncomingMessage, res: http.ServerResponse, urlPath: string): void {
|
|
1199
|
+
const chunkId = urlPath.split("/")[3];
|
|
1200
|
+
this.readBody(req, async (body) => {
|
|
1201
|
+
try {
|
|
1202
|
+
const parsed = JSON.parse(body || "{}");
|
|
1203
|
+
const scope = parsed.scope as string;
|
|
1204
|
+
if (!["private", "local", "team"].includes(scope)) {
|
|
1205
|
+
return this.jsonResponse(res, { ok: false, error: "scope must be 'private', 'local', or 'team'" }, 400);
|
|
1206
|
+
}
|
|
1207
|
+
const db = (this.store as any).db;
|
|
1208
|
+
const chunk = db.prepare("SELECT * FROM chunks WHERE id = ?").get(chunkId) as any;
|
|
1209
|
+
if (!chunk) return this.jsonResponse(res, { ok: false, error: "not_found" }, 404);
|
|
1210
|
+
|
|
1211
|
+
if (chunk.dedup_status && chunk.dedup_status !== "active") {
|
|
1212
|
+
return this.jsonResponse(res, { ok: false, error: "inactive_memory", message: "Merged/duplicate memories cannot be shared" }, 400);
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
const isLocalShared = chunk.owner === "public";
|
|
1216
|
+
const hubMemory = this.getHubMemoryForChunk(chunkId);
|
|
1217
|
+
const isTeamShared = !!hubMemory;
|
|
1218
|
+
const currentScope = isTeamShared ? "team" : isLocalShared ? "local" : "private";
|
|
1219
|
+
|
|
1220
|
+
if (scope === currentScope) {
|
|
1221
|
+
return this.jsonResponse(res, { ok: true, scope, changed: false });
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
let hubSynced = false;
|
|
1225
|
+
|
|
1226
|
+
if (scope === "team") {
|
|
1227
|
+
if (!isTeamShared) {
|
|
1228
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1229
|
+
const refreshedChunk = db.prepare("SELECT * FROM chunks WHERE id = ?").get(chunkId) as any;
|
|
1230
|
+
const response = await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/share", {
|
|
1231
|
+
method: "POST",
|
|
1232
|
+
body: JSON.stringify({ memory: { sourceChunkId: refreshedChunk.id, role: refreshedChunk.role, content: refreshedChunk.content, summary: refreshedChunk.summary, kind: refreshedChunk.kind, groupId: null, visibility: "public" } }),
|
|
1233
|
+
});
|
|
1234
|
+
if (!isLocalShared) this.store.markMemorySharedLocally(chunkId);
|
|
1235
|
+
if (hubClient.userId) {
|
|
1236
|
+
const existing = this.store.getHubMemoryBySource(hubClient.userId, chunkId);
|
|
1237
|
+
this.store.upsertHubMemory({
|
|
1238
|
+
id: (response as any)?.memoryId ?? existing?.id ?? crypto.randomUUID(),
|
|
1239
|
+
sourceChunkId: chunkId, sourceUserId: hubClient.userId,
|
|
1240
|
+
role: refreshedChunk.role, content: refreshedChunk.content, summary: refreshedChunk.summary ?? "",
|
|
1241
|
+
kind: refreshedChunk.kind, groupId: null, visibility: "public",
|
|
1242
|
+
createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(),
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
hubSynced = true;
|
|
1246
|
+
} else {
|
|
1247
|
+
if (!isLocalShared) this.store.markMemorySharedLocally(chunkId);
|
|
1248
|
+
}
|
|
1249
|
+
} else if (scope === "local") {
|
|
1250
|
+
if (isTeamShared) {
|
|
1251
|
+
try {
|
|
1252
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1253
|
+
await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/unshare", {
|
|
1254
|
+
method: "POST", body: JSON.stringify({ sourceChunkId: chunkId }),
|
|
1255
|
+
});
|
|
1256
|
+
if (hubClient.userId) this.store.deleteHubMemoryBySource(hubClient.userId, chunkId);
|
|
1257
|
+
hubSynced = true;
|
|
1258
|
+
} catch (err) { this.log.warn(`Failed to unshare memory from team: ${err}`); }
|
|
1259
|
+
}
|
|
1260
|
+
if (!isLocalShared) this.store.markMemorySharedLocally(chunkId);
|
|
1261
|
+
} else {
|
|
1262
|
+
if (isTeamShared) {
|
|
1263
|
+
try {
|
|
1264
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1265
|
+
await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/unshare", {
|
|
1266
|
+
method: "POST", body: JSON.stringify({ sourceChunkId: chunkId }),
|
|
1267
|
+
});
|
|
1268
|
+
if (hubClient.userId) this.store.deleteHubMemoryBySource(hubClient.userId, chunkId);
|
|
1269
|
+
hubSynced = true;
|
|
1270
|
+
} catch (err) { this.log.warn(`Failed to unshare memory from team: ${err}`); }
|
|
1271
|
+
}
|
|
1272
|
+
if (isLocalShared) this.store.unmarkMemorySharedLocally(chunkId);
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
this.jsonResponse(res, { ok: true, scope, changed: true, hubSynced });
|
|
1276
|
+
} catch (err) {
|
|
1277
|
+
this.jsonResponse(res, { ok: false, error: String(err) }, 500);
|
|
1278
|
+
}
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
private handleTaskScope(req: http.IncomingMessage, res: http.ServerResponse, urlPath: string): void {
|
|
1283
|
+
const taskId = urlPath.split("/")[3];
|
|
1284
|
+
this.readBody(req, async (body) => {
|
|
1285
|
+
try {
|
|
1286
|
+
const parsed = JSON.parse(body || "{}");
|
|
1287
|
+
const scope = parsed.scope as string;
|
|
1288
|
+
if (!["private", "local", "team"].includes(scope)) {
|
|
1289
|
+
return this.jsonResponse(res, { ok: false, error: "scope must be 'private', 'local', or 'team'" }, 400);
|
|
1290
|
+
}
|
|
1291
|
+
const task = this.store.getTask(taskId);
|
|
1292
|
+
if (!task) return this.jsonResponse(res, { ok: false, error: "task_not_found" }, 404);
|
|
1293
|
+
|
|
1294
|
+
if (scope !== "private" && task.status !== "completed") {
|
|
1295
|
+
return this.jsonResponse(res, { ok: false, error: "only_completed_tasks_can_be_shared" }, 400);
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
const isLocalShared = task.owner === "public";
|
|
1299
|
+
const hubTask = this.getHubTaskForLocal(taskId);
|
|
1300
|
+
const isTeamShared = !!hubTask;
|
|
1301
|
+
const currentScope = isTeamShared ? "team" : isLocalShared ? "local" : "private";
|
|
1302
|
+
|
|
1303
|
+
if (scope === currentScope) {
|
|
1304
|
+
return this.jsonResponse(res, { ok: true, scope, changed: false });
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
let hubSynced = false;
|
|
1308
|
+
|
|
1309
|
+
if (scope === "team") {
|
|
1310
|
+
if (!isTeamShared) {
|
|
1311
|
+
const chunks = this.store.getChunksByTask(taskId);
|
|
1312
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1313
|
+
const refreshedTask = this.store.getTask(taskId)!;
|
|
1314
|
+
const response = await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/share", {
|
|
1315
|
+
method: "POST",
|
|
1316
|
+
body: JSON.stringify({
|
|
1317
|
+
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() },
|
|
1318
|
+
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() })),
|
|
1319
|
+
}),
|
|
1320
|
+
});
|
|
1321
|
+
if (hubClient.userId) {
|
|
1322
|
+
const existing = this.store.getHubTaskBySource(hubClient.userId, taskId);
|
|
1323
|
+
this.store.upsertHubTask({
|
|
1324
|
+
id: (response as any)?.taskId ?? existing?.id ?? crypto.randomUUID(),
|
|
1325
|
+
sourceTaskId: taskId, sourceUserId: hubClient.userId, title: refreshedTask.title ?? "",
|
|
1326
|
+
summary: refreshedTask.summary ?? "", groupId: null, visibility: "public",
|
|
1327
|
+
createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(),
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
hubSynced = true;
|
|
1331
|
+
}
|
|
1332
|
+
if (!isLocalShared) {
|
|
1333
|
+
const originalOwner = task.owner;
|
|
1334
|
+
const db = (this.store as any).db;
|
|
1335
|
+
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());
|
|
1336
|
+
db.prepare("UPDATE tasks SET owner = 'public' WHERE id = ?").run(taskId);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
if (scope === "local") {
|
|
1341
|
+
if (!isLocalShared) {
|
|
1342
|
+
const originalOwner = task.owner;
|
|
1343
|
+
const db = (this.store as any).db;
|
|
1344
|
+
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());
|
|
1345
|
+
db.prepare("UPDATE tasks SET owner = 'public' WHERE id = ?").run(taskId);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
if (scope === "local" && isTeamShared) {
|
|
1350
|
+
try {
|
|
1351
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1352
|
+
await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/unshare", {
|
|
1353
|
+
method: "POST", body: JSON.stringify({ sourceTaskId: taskId }),
|
|
1354
|
+
});
|
|
1355
|
+
if (hubClient.userId) this.store.deleteHubTaskBySource(hubClient.userId, taskId);
|
|
1356
|
+
hubSynced = true;
|
|
1357
|
+
} catch (err) { this.log.warn(`Failed to unshare task from team: ${err}`); }
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
if (scope === "private") {
|
|
1361
|
+
if (isTeamShared) {
|
|
1362
|
+
try {
|
|
1363
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1364
|
+
await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/unshare", {
|
|
1365
|
+
method: "POST", body: JSON.stringify({ sourceTaskId: taskId }),
|
|
1366
|
+
});
|
|
1367
|
+
if (hubClient.userId) this.store.deleteHubTaskBySource(hubClient.userId, taskId);
|
|
1368
|
+
hubSynced = true;
|
|
1369
|
+
} catch (err) { this.log.warn(`Failed to unshare task from team: ${err}`); }
|
|
1370
|
+
}
|
|
1371
|
+
if (isLocalShared) {
|
|
1372
|
+
const db = (this.store as any).db;
|
|
1373
|
+
const shared = db.prepare("SELECT original_owner FROM local_shared_tasks WHERE task_id = ?").get(taskId) as any;
|
|
1374
|
+
const restoreOwner = shared?.original_owner ?? task.owner;
|
|
1375
|
+
if (restoreOwner && restoreOwner !== "public") {
|
|
1376
|
+
db.prepare("UPDATE tasks SET owner = ? WHERE id = ?").run(restoreOwner, taskId);
|
|
1377
|
+
}
|
|
1378
|
+
db.prepare("DELETE FROM local_shared_tasks WHERE task_id = ?").run(taskId);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
this.jsonResponse(res, { ok: true, scope, changed: true, hubSynced });
|
|
1383
|
+
} catch (err) {
|
|
1384
|
+
this.jsonResponse(res, { ok: false, error: String(err) }, 500);
|
|
1385
|
+
}
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
private handleSkillScope(req: http.IncomingMessage, res: http.ServerResponse, urlPath: string): void {
|
|
1390
|
+
const skillId = urlPath.split("/")[3];
|
|
1391
|
+
this.readBody(req, async (body) => {
|
|
1392
|
+
try {
|
|
1393
|
+
const parsed = JSON.parse(body || "{}");
|
|
1394
|
+
const scope = parsed.scope as string;
|
|
1395
|
+
if (!["private", "local", "team"].includes(scope)) {
|
|
1396
|
+
return this.jsonResponse(res, { ok: false, error: "scope must be 'private', 'local', or 'team'" }, 400);
|
|
1397
|
+
}
|
|
1398
|
+
const skill = this.store.getSkill(skillId);
|
|
1399
|
+
if (!skill) return this.jsonResponse(res, { ok: false, error: "skill_not_found" }, 404);
|
|
1400
|
+
|
|
1401
|
+
if (scope !== "private" && skill.status !== "active") {
|
|
1402
|
+
return this.jsonResponse(res, { ok: false, error: "only_active_skills_can_be_shared" }, 400);
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
const isLocalShared = skill.visibility === "public";
|
|
1406
|
+
const hubSkill = this.getHubSkillForLocal(skillId);
|
|
1407
|
+
const isTeamShared = !!hubSkill;
|
|
1408
|
+
const currentScope = isTeamShared ? "team" : isLocalShared ? "local" : "private";
|
|
1409
|
+
|
|
1410
|
+
if (scope === currentScope) {
|
|
1411
|
+
return this.jsonResponse(res, { ok: true, scope, changed: false });
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
let hubSynced = false;
|
|
1415
|
+
|
|
1416
|
+
if (scope === "team") {
|
|
1417
|
+
if (!isTeamShared) {
|
|
1418
|
+
const bundle = buildSkillBundleForHub(this.store, skillId);
|
|
1419
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1420
|
+
const response = await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/publish", {
|
|
1421
|
+
method: "POST",
|
|
1422
|
+
body: JSON.stringify({ visibility: "public", groupId: null, metadata: bundle.metadata, bundle: bundle.bundle }),
|
|
1423
|
+
});
|
|
1424
|
+
if (hubClient.userId) {
|
|
1425
|
+
const existing = this.store.getHubSkillBySource(hubClient.userId, skillId);
|
|
1426
|
+
this.store.upsertHubSkill({
|
|
1427
|
+
id: (response as any)?.skillId ?? existing?.id ?? crypto.randomUUID(),
|
|
1428
|
+
sourceSkillId: skillId, sourceUserId: hubClient.userId,
|
|
1429
|
+
name: skill.name, description: skill.description, version: skill.version,
|
|
1430
|
+
groupId: null, visibility: "public",
|
|
1431
|
+
bundle: JSON.stringify(bundle.bundle), qualityScore: skill.qualityScore,
|
|
1432
|
+
createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(),
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
hubSynced = true;
|
|
1436
|
+
}
|
|
1437
|
+
if (!isLocalShared) this.store.setSkillVisibility(skillId, "public");
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
if (scope === "local") {
|
|
1441
|
+
if (!isLocalShared) this.store.setSkillVisibility(skillId, "public");
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
if (scope === "local" && isTeamShared) {
|
|
1445
|
+
try {
|
|
1446
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1447
|
+
await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/unpublish", {
|
|
1448
|
+
method: "POST", body: JSON.stringify({ sourceSkillId: skillId }),
|
|
1449
|
+
});
|
|
1450
|
+
if (hubClient.userId) this.store.deleteHubSkillBySource(hubClient.userId, skillId);
|
|
1451
|
+
hubSynced = true;
|
|
1452
|
+
} catch (err) { this.log.warn(`Failed to unpublish skill from team: ${err}`); }
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
if (scope === "private") {
|
|
1456
|
+
if (isTeamShared) {
|
|
1457
|
+
try {
|
|
1458
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1459
|
+
await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/unpublish", {
|
|
1460
|
+
method: "POST", body: JSON.stringify({ sourceSkillId: skillId }),
|
|
1461
|
+
});
|
|
1462
|
+
if (hubClient.userId) this.store.deleteHubSkillBySource(hubClient.userId, skillId);
|
|
1463
|
+
hubSynced = true;
|
|
1464
|
+
} catch (err) { this.log.warn(`Failed to unpublish skill from team: ${err}`); }
|
|
1465
|
+
}
|
|
1466
|
+
if (isLocalShared) this.store.setSkillVisibility(skillId, "private");
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
this.jsonResponse(res, { ok: true, scope, changed: true, hubSynced });
|
|
1470
|
+
} catch (err) {
|
|
1471
|
+
this.jsonResponse(res, { ok: false, error: String(err) }, 500);
|
|
1472
|
+
}
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
private getHubMemoryForChunk(chunkId: string): any {
|
|
1477
|
+
const db = (this.store as any).db;
|
|
1478
|
+
return db.prepare("SELECT * FROM hub_memories WHERE source_chunk_id = ? LIMIT 1").get(chunkId);
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
private getHubTaskForLocal(taskId: string): any {
|
|
1482
|
+
const db = (this.store as any).db;
|
|
1483
|
+
return db.prepare("SELECT * FROM hub_tasks WHERE source_task_id = ? LIMIT 1").get(taskId);
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
private getHubSkillForLocal(skillId: string): any {
|
|
1487
|
+
const db = (this.store as any).db;
|
|
1488
|
+
return db.prepare("SELECT * FROM hub_skills WHERE source_skill_id = ? LIMIT 1").get(skillId);
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1061
1491
|
private handleDeleteSession(res: http.ServerResponse, url: URL): void {
|
|
1062
1492
|
const key = url.searchParams.get("key");
|
|
1063
1493
|
if (!key) { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Missing key" })); return; }
|
|
@@ -1093,7 +1523,8 @@ export class ViewerServer {
|
|
|
1093
1523
|
|
|
1094
1524
|
private getOpenClawConfigPath(): string {
|
|
1095
1525
|
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
1096
|
-
|
|
1526
|
+
const ocHome = process.env.OPENCLAW_STATE_DIR || path.join(home, ".openclaw");
|
|
1527
|
+
return path.join(ocHome, "openclaw.json");
|
|
1097
1528
|
}
|
|
1098
1529
|
|
|
1099
1530
|
private getPluginEntryConfig(raw: any): Record<string, unknown> {
|
|
@@ -1157,8 +1588,7 @@ export class ViewerServer {
|
|
|
1157
1588
|
base.connection.connected = true;
|
|
1158
1589
|
base.connection.hubUrl = resolvedHubUrl ?? undefined;
|
|
1159
1590
|
|
|
1160
|
-
|
|
1161
|
-
let adminUser: any = { username: "hub-admin", role: "admin", groups: [] };
|
|
1591
|
+
let adminUser: any = { username: "hub-admin", role: "admin" };
|
|
1162
1592
|
try {
|
|
1163
1593
|
const hub = this.resolveHubConnection();
|
|
1164
1594
|
if (hub) {
|
|
@@ -1168,7 +1598,6 @@ export class ViewerServer {
|
|
|
1168
1598
|
id: me.id,
|
|
1169
1599
|
username: me.username ?? "hub-admin",
|
|
1170
1600
|
role: me.role ?? "admin",
|
|
1171
|
-
groups: Array.isArray(me.groups) ? me.groups : [],
|
|
1172
1601
|
};
|
|
1173
1602
|
}
|
|
1174
1603
|
}
|
|
@@ -1201,6 +1630,9 @@ export class ViewerServer {
|
|
|
1201
1630
|
if (status.user?.status === "rejected") {
|
|
1202
1631
|
output.connection.rejected = true;
|
|
1203
1632
|
}
|
|
1633
|
+
if (status.user?.status === "removed") {
|
|
1634
|
+
output.connection.removed = true;
|
|
1635
|
+
}
|
|
1204
1636
|
if (status.connected && status.hubUrl) {
|
|
1205
1637
|
try {
|
|
1206
1638
|
const info = await fetch(`${status.hubUrl}/api/v1/hub/info`).then((r) => (r.ok ? r.json() : null)).catch(() => null) as any;
|
|
@@ -1269,6 +1701,67 @@ export class ViewerServer {
|
|
|
1269
1701
|
});
|
|
1270
1702
|
}
|
|
1271
1703
|
|
|
1704
|
+
private handleSharingChangeRole(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
1705
|
+
this.readBody(req, async (body) => {
|
|
1706
|
+
if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
1707
|
+
try {
|
|
1708
|
+
const parsed = JSON.parse(body || "{}");
|
|
1709
|
+
const hub = this.resolveHubConnection();
|
|
1710
|
+
if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1711
|
+
const result = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/change-role", {
|
|
1712
|
+
method: "POST",
|
|
1713
|
+
body: JSON.stringify({ userId: parsed.userId, role: parsed.role }),
|
|
1714
|
+
});
|
|
1715
|
+
this.jsonResponse(res, { ok: true, result });
|
|
1716
|
+
} catch (err) {
|
|
1717
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1718
|
+
}
|
|
1719
|
+
});
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
private handleSharingRemoveUser(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
1723
|
+
this.readBody(req, async (body) => {
|
|
1724
|
+
if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
1725
|
+
try {
|
|
1726
|
+
const parsed = JSON.parse(body || "{}");
|
|
1727
|
+
const hub = this.resolveHubConnection();
|
|
1728
|
+
if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1729
|
+
const result = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/remove-user", {
|
|
1730
|
+
method: "POST",
|
|
1731
|
+
body: JSON.stringify({ userId: parsed.userId, cleanResources: parsed.cleanResources === true }),
|
|
1732
|
+
});
|
|
1733
|
+
this.jsonResponse(res, { ok: true, result });
|
|
1734
|
+
} catch (err) {
|
|
1735
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1736
|
+
}
|
|
1737
|
+
});
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
private handleAdminRenameUser(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
1741
|
+
this.readBody(req, async (body) => {
|
|
1742
|
+
if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
1743
|
+
try {
|
|
1744
|
+
const parsed = JSON.parse(body || "{}");
|
|
1745
|
+
const hub = this.resolveHubConnection();
|
|
1746
|
+
if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1747
|
+
const result = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/rename-user", {
|
|
1748
|
+
method: "POST",
|
|
1749
|
+
body: JSON.stringify({ userId: parsed.userId, username: parsed.username }),
|
|
1750
|
+
});
|
|
1751
|
+
this.jsonResponse(res, { ok: true, result });
|
|
1752
|
+
} catch (err) {
|
|
1753
|
+
const errStr = String(err);
|
|
1754
|
+
if (errStr.includes("username_taken")) {
|
|
1755
|
+
this.jsonResponse(res, { ok: false, error: "username_taken" });
|
|
1756
|
+
} else if (errStr.includes("invalid_params")) {
|
|
1757
|
+
this.jsonResponse(res, { ok: false, error: "invalid_params" });
|
|
1758
|
+
} else {
|
|
1759
|
+
this.jsonResponse(res, { ok: false, error: errStr });
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1272
1765
|
private handleRetryJoin(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
1273
1766
|
this.readBody(req, async (_body) => {
|
|
1274
1767
|
if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
@@ -1283,13 +1776,26 @@ export class ViewerServer {
|
|
|
1283
1776
|
}
|
|
1284
1777
|
try {
|
|
1285
1778
|
const hubUrl = normalizeHubUrl(hubAddress);
|
|
1779
|
+
const localIPs = this.getLocalIPs();
|
|
1780
|
+
localIPs.push("127.0.0.1", "localhost", "0.0.0.0");
|
|
1781
|
+
try {
|
|
1782
|
+
const u = new URL(hubUrl);
|
|
1783
|
+
const targetPort = u.port || (u.protocol === "https:" ? "443" : "80");
|
|
1784
|
+
if (localIPs.includes(u.hostname) && targetPort === String(this.port)) {
|
|
1785
|
+
return this.jsonResponse(res, { ok: false, error: "cannot_join_self" });
|
|
1786
|
+
}
|
|
1787
|
+
} catch {}
|
|
1286
1788
|
const os = await import("os");
|
|
1287
|
-
const
|
|
1789
|
+
const nickname = sharing.client?.nickname;
|
|
1790
|
+
const username = nickname || os.userInfo().username || "user";
|
|
1288
1791
|
const hostname = os.hostname() || "unknown";
|
|
1792
|
+
const persisted = this.store.getClientHubConnection();
|
|
1793
|
+
const existingIdentityKey = persisted?.identityKey || "";
|
|
1289
1794
|
const result = await hubRequestJson(hubUrl, "", "/api/v1/hub/join", {
|
|
1290
1795
|
method: "POST",
|
|
1291
|
-
body: JSON.stringify({ teamToken, username, deviceName: hostname }),
|
|
1796
|
+
body: JSON.stringify({ teamToken, username, deviceName: hostname, reapply: true, identityKey: existingIdentityKey }),
|
|
1292
1797
|
}) as any;
|
|
1798
|
+
const returnedIdentityKey = String(result.identityKey || existingIdentityKey || "");
|
|
1293
1799
|
this.store.setClientHubConnection({
|
|
1294
1800
|
hubUrl,
|
|
1295
1801
|
userId: String(result.userId || ""),
|
|
@@ -1297,6 +1803,8 @@ export class ViewerServer {
|
|
|
1297
1803
|
userToken: result.userToken || "",
|
|
1298
1804
|
role: "member",
|
|
1299
1805
|
connectedAt: Date.now(),
|
|
1806
|
+
identityKey: returnedIdentityKey,
|
|
1807
|
+
lastKnownStatus: result.status || "",
|
|
1300
1808
|
});
|
|
1301
1809
|
this.jsonResponse(res, { ok: true, status: result.status || "pending" });
|
|
1302
1810
|
} catch (err) {
|
|
@@ -1309,7 +1817,13 @@ export class ViewerServer {
|
|
|
1309
1817
|
if (!this.ctx) return this.jsonResponse(res, { memories: [], error: "sharing_unavailable" });
|
|
1310
1818
|
try {
|
|
1311
1819
|
const limit = Number(url.searchParams.get("limit") || 40);
|
|
1312
|
-
const
|
|
1820
|
+
const hub = this.resolveHubConnection();
|
|
1821
|
+
let data: any;
|
|
1822
|
+
if (hub) {
|
|
1823
|
+
data = await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/memories?limit=${limit}`);
|
|
1824
|
+
} else {
|
|
1825
|
+
data = await hubListMemories(this.store, this.ctx, { limit });
|
|
1826
|
+
}
|
|
1313
1827
|
this.jsonResponse(res, { memories: Array.isArray(data?.memories) ? data.memories : [] });
|
|
1314
1828
|
} catch (err) {
|
|
1315
1829
|
this.jsonResponse(res, { memories: [], error: String(err) });
|
|
@@ -1320,7 +1834,13 @@ export class ViewerServer {
|
|
|
1320
1834
|
if (!this.ctx) return this.jsonResponse(res, { tasks: [], error: "sharing_unavailable" });
|
|
1321
1835
|
try {
|
|
1322
1836
|
const limit = Number(url.searchParams.get("limit") || 40);
|
|
1323
|
-
const
|
|
1837
|
+
const hub = this.resolveHubConnection();
|
|
1838
|
+
let data: any;
|
|
1839
|
+
if (hub) {
|
|
1840
|
+
data = await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/tasks?limit=${limit}`);
|
|
1841
|
+
} else {
|
|
1842
|
+
data = await hubListTasks(this.store, this.ctx, { limit });
|
|
1843
|
+
}
|
|
1324
1844
|
this.jsonResponse(res, { tasks: Array.isArray(data?.tasks) ? data.tasks : [] });
|
|
1325
1845
|
} catch (err) {
|
|
1326
1846
|
this.jsonResponse(res, { tasks: [], error: String(err) });
|
|
@@ -1331,7 +1851,13 @@ export class ViewerServer {
|
|
|
1331
1851
|
if (!this.ctx) return this.jsonResponse(res, { skills: [], error: "sharing_unavailable" });
|
|
1332
1852
|
try {
|
|
1333
1853
|
const limit = Number(url.searchParams.get("limit") || 40);
|
|
1334
|
-
const
|
|
1854
|
+
const hub = this.resolveHubConnection();
|
|
1855
|
+
let data: any;
|
|
1856
|
+
if (hub) {
|
|
1857
|
+
data = await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/skills/list?limit=${limit}`);
|
|
1858
|
+
} else {
|
|
1859
|
+
data = await hubListSkills(this.store, this.ctx, { limit });
|
|
1860
|
+
}
|
|
1335
1861
|
this.jsonResponse(res, { skills: Array.isArray(data?.skills) ? data.skills : [] });
|
|
1336
1862
|
} catch (err) {
|
|
1337
1863
|
this.jsonResponse(res, { skills: [], error: String(err) });
|
|
@@ -1347,13 +1873,21 @@ export class ViewerServer {
|
|
|
1347
1873
|
const query = String(parsed.query || "");
|
|
1348
1874
|
const role = typeof parsed.role === "string" ? parsed.role : undefined;
|
|
1349
1875
|
const maxResults = typeof parsed.maxResults === "number" ? parsed.maxResults : 10;
|
|
1350
|
-
const scope = parsed.scope === "group" || parsed.scope === "all" ? parsed.scope : "local";
|
|
1876
|
+
const scope = parsed.scope === "group" || parsed.scope === "all" || parsed.scope === "hub" ? (parsed.scope === "hub" ? "all" : parsed.scope) : "local";
|
|
1351
1877
|
const local = this.searchLocalViewerMemories(query, { role, maxResults });
|
|
1352
1878
|
if (scope === "local") {
|
|
1353
1879
|
return this.jsonResponse(res, { local: { hits: local.hits, meta: local.meta }, hub: emptyHub });
|
|
1354
1880
|
}
|
|
1355
1881
|
try {
|
|
1356
|
-
const
|
|
1882
|
+
const conn = this.resolveHubConnection();
|
|
1883
|
+
let hub: any;
|
|
1884
|
+
if (conn) {
|
|
1885
|
+
hub = await hubRequestJson(conn.hubUrl, conn.userToken, "/api/v1/hub/search", {
|
|
1886
|
+
method: "POST", body: JSON.stringify({ query, maxResults, scope }),
|
|
1887
|
+
});
|
|
1888
|
+
} else {
|
|
1889
|
+
hub = await hubSearchMemories(this.store, this.ctx!, { query, maxResults, scope });
|
|
1890
|
+
}
|
|
1357
1891
|
this.jsonResponse(res, { local: { hits: local.hits, meta: local.meta }, hub });
|
|
1358
1892
|
} catch (err) {
|
|
1359
1893
|
this.jsonResponse(res, { local: { hits: local.hits, meta: local.meta }, hub: emptyHub, error: String(err) });
|
|
@@ -1720,117 +2254,6 @@ export class ViewerServer {
|
|
|
1720
2254
|
return resolveHubClient(this.store, this.ctx);
|
|
1721
2255
|
}
|
|
1722
2256
|
|
|
1723
|
-
private extractGroupId(path: string): string {
|
|
1724
|
-
const m = path.match(/\/api\/sharing\/groups\/([^/]+)/);
|
|
1725
|
-
return m ? decodeURIComponent(m[1]) : "";
|
|
1726
|
-
}
|
|
1727
|
-
|
|
1728
|
-
private async serveSharingGroups(res: http.ServerResponse): Promise<void> {
|
|
1729
|
-
const hub = this.resolveHubConnection();
|
|
1730
|
-
if (!hub) return this.jsonResponse(res, { groups: [], error: "not_configured" });
|
|
1731
|
-
try {
|
|
1732
|
-
const data = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/groups", { method: "GET" }) as any;
|
|
1733
|
-
this.jsonResponse(res, { groups: Array.isArray(data?.groups) ? data.groups : [] });
|
|
1734
|
-
} catch (err) {
|
|
1735
|
-
this.jsonResponse(res, { groups: [], error: String(err) });
|
|
1736
|
-
}
|
|
1737
|
-
}
|
|
1738
|
-
|
|
1739
|
-
private handleSharingGroupCreate(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
1740
|
-
this.readBody(req, async (body) => {
|
|
1741
|
-
const hub = this.resolveHubConnection();
|
|
1742
|
-
if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1743
|
-
try {
|
|
1744
|
-
const parsed = JSON.parse(body || "{}");
|
|
1745
|
-
const data = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/groups", {
|
|
1746
|
-
method: "POST",
|
|
1747
|
-
body: JSON.stringify({ name: parsed.name, description: parsed.description }),
|
|
1748
|
-
}) as any;
|
|
1749
|
-
this.jsonResponse(res, { ok: true, ...data });
|
|
1750
|
-
} catch (err) {
|
|
1751
|
-
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1752
|
-
}
|
|
1753
|
-
});
|
|
1754
|
-
}
|
|
1755
|
-
|
|
1756
|
-
private handleSharingGroupUpdate(req: http.IncomingMessage, res: http.ServerResponse, p: string): void {
|
|
1757
|
-
this.readBody(req, async (body) => {
|
|
1758
|
-
const hub = this.resolveHubConnection();
|
|
1759
|
-
if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1760
|
-
const groupId = this.extractGroupId(p);
|
|
1761
|
-
try {
|
|
1762
|
-
const parsed = JSON.parse(body || "{}");
|
|
1763
|
-
await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}`, {
|
|
1764
|
-
method: "PUT",
|
|
1765
|
-
body: JSON.stringify({ name: parsed.name, description: parsed.description }),
|
|
1766
|
-
});
|
|
1767
|
-
this.jsonResponse(res, { ok: true });
|
|
1768
|
-
} catch (err) {
|
|
1769
|
-
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1770
|
-
}
|
|
1771
|
-
});
|
|
1772
|
-
}
|
|
1773
|
-
|
|
1774
|
-
private async handleSharingGroupDelete(res: http.ServerResponse, p: string): Promise<void> {
|
|
1775
|
-
const hub = this.resolveHubConnection();
|
|
1776
|
-
if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1777
|
-
const groupId = this.extractGroupId(p);
|
|
1778
|
-
try {
|
|
1779
|
-
await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}`, { method: "DELETE" });
|
|
1780
|
-
this.jsonResponse(res, { ok: true });
|
|
1781
|
-
} catch (err) {
|
|
1782
|
-
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1783
|
-
}
|
|
1784
|
-
}
|
|
1785
|
-
|
|
1786
|
-
private async serveSharingGroupMembers(res: http.ServerResponse, p: string): Promise<void> {
|
|
1787
|
-
const hub = this.resolveHubConnection();
|
|
1788
|
-
if (!hub) return this.jsonResponse(res, { members: [], error: "not_configured" });
|
|
1789
|
-
const groupId = this.extractGroupId(p);
|
|
1790
|
-
try {
|
|
1791
|
-
const data = await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}`, { method: "GET" }) as any;
|
|
1792
|
-
this.jsonResponse(res, { members: Array.isArray(data?.members) ? data.members : [] });
|
|
1793
|
-
} catch (err) {
|
|
1794
|
-
this.jsonResponse(res, { members: [], error: String(err) });
|
|
1795
|
-
}
|
|
1796
|
-
}
|
|
1797
|
-
|
|
1798
|
-
private handleSharingGroupAddMember(req: http.IncomingMessage, res: http.ServerResponse, p: string): void {
|
|
1799
|
-
this.readBody(req, async (body) => {
|
|
1800
|
-
const hub = this.resolveHubConnection();
|
|
1801
|
-
if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1802
|
-
const groupId = this.extractGroupId(p);
|
|
1803
|
-
try {
|
|
1804
|
-
const parsed = JSON.parse(body || "{}");
|
|
1805
|
-
await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}/members`, {
|
|
1806
|
-
method: "POST",
|
|
1807
|
-
body: JSON.stringify({ userId: parsed.userId }),
|
|
1808
|
-
});
|
|
1809
|
-
this.jsonResponse(res, { ok: true });
|
|
1810
|
-
} catch (err) {
|
|
1811
|
-
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1812
|
-
}
|
|
1813
|
-
});
|
|
1814
|
-
}
|
|
1815
|
-
|
|
1816
|
-
private handleSharingGroupRemoveMember(req: http.IncomingMessage, res: http.ServerResponse, p: string): void {
|
|
1817
|
-
this.readBody(req, async (body) => {
|
|
1818
|
-
const hub = this.resolveHubConnection();
|
|
1819
|
-
if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1820
|
-
const groupId = this.extractGroupId(p);
|
|
1821
|
-
try {
|
|
1822
|
-
const parsed = JSON.parse(body || "{}");
|
|
1823
|
-
await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}/members`, {
|
|
1824
|
-
method: "DELETE",
|
|
1825
|
-
body: JSON.stringify({ userId: parsed.userId }),
|
|
1826
|
-
});
|
|
1827
|
-
this.jsonResponse(res, { ok: true });
|
|
1828
|
-
} catch (err) {
|
|
1829
|
-
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1830
|
-
}
|
|
1831
|
-
});
|
|
1832
|
-
}
|
|
1833
|
-
|
|
1834
2257
|
private async serveSharingUsers(res: http.ServerResponse): Promise<void> {
|
|
1835
2258
|
const hub = this.resolveHubConnection();
|
|
1836
2259
|
if (!hub) return this.jsonResponse(res, { users: [], error: "not_configured" });
|
|
@@ -1849,7 +2272,14 @@ export class ViewerServer {
|
|
|
1849
2272
|
if (!hub) return this.jsonResponse(res, { tasks: [], error: "not_configured" });
|
|
1850
2273
|
try {
|
|
1851
2274
|
const data = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/shared-tasks", { method: "GET" }) as any;
|
|
1852
|
-
|
|
2275
|
+
const tasks = Array.isArray(data?.tasks) ? data.tasks : [];
|
|
2276
|
+
for (const tk of tasks) {
|
|
2277
|
+
if (!tk.summary && tk.sourceTaskId) {
|
|
2278
|
+
const local = this.store.getTask(tk.sourceTaskId);
|
|
2279
|
+
if (local) { tk.summary = local.summary; tk.title = tk.title || local.title; }
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
this.jsonResponse(res, { tasks });
|
|
1853
2283
|
} catch (err) {
|
|
1854
2284
|
this.jsonResponse(res, { tasks: [], error: String(err) });
|
|
1855
2285
|
}
|
|
@@ -1867,12 +2297,47 @@ export class ViewerServer {
|
|
|
1867
2297
|
}
|
|
1868
2298
|
}
|
|
1869
2299
|
|
|
2300
|
+
private async serveHubTaskDetail(res: http.ServerResponse, p: string): Promise<void> {
|
|
2301
|
+
const hub = this.resolveHubConnection();
|
|
2302
|
+
if (!hub) return this.jsonResponse(res, { error: "not_configured" }, 500);
|
|
2303
|
+
const m = p.match(/^\/api\/admin\/shared-tasks\/([^/]+)\/detail$/);
|
|
2304
|
+
if (!m) return this.jsonResponse(res, { error: "bad_request" }, 400);
|
|
2305
|
+
const taskId = decodeURIComponent(m[1]);
|
|
2306
|
+
try {
|
|
2307
|
+
const data = await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/shared-tasks/${encodeURIComponent(taskId)}/detail`, { method: "GET" }) as any;
|
|
2308
|
+
this.jsonResponse(res, data);
|
|
2309
|
+
} catch (err) {
|
|
2310
|
+
this.jsonResponse(res, { error: String(err) }, 500);
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
private async serveHubSkillDetail(res: http.ServerResponse, p: string): Promise<void> {
|
|
2315
|
+
const hub = this.resolveHubConnection();
|
|
2316
|
+
if (!hub) return this.jsonResponse(res, { error: "not_configured" }, 500);
|
|
2317
|
+
const m = p.match(/^\/api\/admin\/shared-skills\/([^/]+)\/detail$/);
|
|
2318
|
+
if (!m) return this.jsonResponse(res, { error: "bad_request" }, 400);
|
|
2319
|
+
const skillId = decodeURIComponent(m[1]);
|
|
2320
|
+
try {
|
|
2321
|
+
const data = await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/shared-skills/${encodeURIComponent(skillId)}/detail`, { method: "GET" }) as any;
|
|
2322
|
+
this.jsonResponse(res, data);
|
|
2323
|
+
} catch (err) {
|
|
2324
|
+
this.jsonResponse(res, { error: String(err) }, 500);
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
|
|
1870
2328
|
private async serveAdminSharedSkills(res: http.ServerResponse): Promise<void> {
|
|
1871
2329
|
const hub = this.resolveHubConnection();
|
|
1872
2330
|
if (!hub) return this.jsonResponse(res, { skills: [], error: "not_configured" });
|
|
1873
2331
|
try {
|
|
1874
2332
|
const data = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/shared-skills", { method: "GET" }) as any;
|
|
1875
|
-
|
|
2333
|
+
const skills = Array.isArray(data?.skills) ? data.skills : [];
|
|
2334
|
+
for (const sk of skills) {
|
|
2335
|
+
if (!sk.description && sk.sourceSkillId) {
|
|
2336
|
+
const local = this.store.getSkill(sk.sourceSkillId);
|
|
2337
|
+
if (local) { sk.description = sk.description || local.description; sk.name = sk.name || local.name; }
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
this.jsonResponse(res, { skills });
|
|
1876
2341
|
} catch (err) {
|
|
1877
2342
|
this.jsonResponse(res, { skills: [], error: String(err) });
|
|
1878
2343
|
}
|
|
@@ -1895,7 +2360,14 @@ export class ViewerServer {
|
|
|
1895
2360
|
if (!hub) return this.jsonResponse(res, { memories: [], error: "not_configured" });
|
|
1896
2361
|
try {
|
|
1897
2362
|
const data = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/shared-memories", { method: "GET" }) as any;
|
|
1898
|
-
|
|
2363
|
+
const memories = Array.isArray(data?.memories) ? data.memories : [];
|
|
2364
|
+
for (const m of memories) {
|
|
2365
|
+
if (!m.content && m.sourceChunkId) {
|
|
2366
|
+
const local = this.store.getChunk(m.sourceChunkId);
|
|
2367
|
+
if (local) { m.content = local.content; if (!m.summary && local.summary) m.summary = local.summary; }
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
this.jsonResponse(res, { memories });
|
|
1899
2371
|
} catch (err) {
|
|
1900
2372
|
this.jsonResponse(res, { memories: [], error: String(err) });
|
|
1901
2373
|
}
|
|
@@ -1913,6 +2385,120 @@ export class ViewerServer {
|
|
|
1913
2385
|
}
|
|
1914
2386
|
}
|
|
1915
2387
|
|
|
2388
|
+
private async serveSharingNotifications(res: http.ServerResponse, url: URL): Promise<void> {
|
|
2389
|
+
const hub = this.resolveHubConnection();
|
|
2390
|
+
if (!hub) return this.jsonResponse(res, { notifications: [], unreadCount: 0 });
|
|
2391
|
+
try {
|
|
2392
|
+
const unread = url.searchParams.get("unread") === "1" ? "?unread=1" : "";
|
|
2393
|
+
const data = await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/notifications${unread}`) as any;
|
|
2394
|
+
this.jsonResponse(res, data);
|
|
2395
|
+
} catch {
|
|
2396
|
+
this.jsonResponse(res, { notifications: [], unreadCount: 0 });
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
private handleSharingNotificationsRead(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
2401
|
+
const hub = this.resolveHubConnection();
|
|
2402
|
+
if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
2403
|
+
this.readBody(req, async (raw) => {
|
|
2404
|
+
try {
|
|
2405
|
+
const body = JSON.parse(raw || "{}");
|
|
2406
|
+
await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications/read", { method: "POST", body: JSON.stringify(body) });
|
|
2407
|
+
this.jsonResponse(res, { ok: true });
|
|
2408
|
+
try {
|
|
2409
|
+
const data = (await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications?unread=1")) as any;
|
|
2410
|
+
const count = data?.unreadCount ?? 0;
|
|
2411
|
+
this.lastKnownNotifCount = count;
|
|
2412
|
+
this.broadcastNotifSSE({ type: "update", unreadCount: count });
|
|
2413
|
+
} catch { /* best effort */ }
|
|
2414
|
+
} catch (err) {
|
|
2415
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
2416
|
+
}
|
|
2417
|
+
});
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
private handleSharingNotificationsClear(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
2421
|
+
const hub = this.resolveHubConnection();
|
|
2422
|
+
if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
2423
|
+
this.readBody(req, async () => {
|
|
2424
|
+
try {
|
|
2425
|
+
await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications/clear", { method: "POST", body: "{}" });
|
|
2426
|
+
this.jsonResponse(res, { ok: true });
|
|
2427
|
+
this.broadcastNotifSSE({ type: "cleared", unreadCount: 0 });
|
|
2428
|
+
} catch (err) {
|
|
2429
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
2430
|
+
}
|
|
2431
|
+
});
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2434
|
+
private handleNotifSSE(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
2435
|
+
res.writeHead(200, {
|
|
2436
|
+
"Content-Type": "text/event-stream",
|
|
2437
|
+
"Cache-Control": "no-cache",
|
|
2438
|
+
Connection: "keep-alive",
|
|
2439
|
+
"Access-Control-Allow-Origin": "*",
|
|
2440
|
+
});
|
|
2441
|
+
res.write("data: {\"type\":\"connected\"}\n\n");
|
|
2442
|
+
this.notifSSEClients.push(res);
|
|
2443
|
+
if (!this.notifPollTimer) this.startNotifPoll();
|
|
2444
|
+
req.on("close", () => {
|
|
2445
|
+
this.notifSSEClients = this.notifSSEClients.filter((c) => c !== res);
|
|
2446
|
+
if (this.notifSSEClients.length === 0) this.stopNotifPoll();
|
|
2447
|
+
});
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
private broadcastNotifSSE(data: Record<string, unknown>): void {
|
|
2451
|
+
const msg = `data: ${JSON.stringify(data)}\n\n`;
|
|
2452
|
+
this.notifSSEClients = this.notifSSEClients.filter((c) => {
|
|
2453
|
+
try { c.write(msg); return true; } catch { return false; }
|
|
2454
|
+
});
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
private startNotifPoll(): void {
|
|
2458
|
+
this.stopNotifPoll();
|
|
2459
|
+
const tick = async () => {
|
|
2460
|
+
const hub = this.resolveHubConnection();
|
|
2461
|
+
if (!hub) return;
|
|
2462
|
+
try {
|
|
2463
|
+
const data = (await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications?unread=1")) as any;
|
|
2464
|
+
const count = data?.unreadCount ?? 0;
|
|
2465
|
+
if (count !== this.lastKnownNotifCount) {
|
|
2466
|
+
this.lastKnownNotifCount = count;
|
|
2467
|
+
this.broadcastNotifSSE({ type: "update", unreadCount: count });
|
|
2468
|
+
}
|
|
2469
|
+
} catch { /* ignore */ }
|
|
2470
|
+
};
|
|
2471
|
+
tick();
|
|
2472
|
+
this.notifPollTimer = setInterval(tick, 3000);
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
private stopNotifPoll(): void {
|
|
2476
|
+
if (this.notifPollTimer) { clearInterval(this.notifPollTimer); this.notifPollTimer = undefined; }
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
private startHubHeartbeat(): void {
|
|
2480
|
+
this.stopHubHeartbeat();
|
|
2481
|
+
const sendHeartbeat = async () => {
|
|
2482
|
+
try {
|
|
2483
|
+
const hub = this.resolveHubConnection();
|
|
2484
|
+
if (!hub) {
|
|
2485
|
+
const persisted = this.store.getClientHubConnection();
|
|
2486
|
+
if (persisted?.hubUrl && persisted?.userToken) {
|
|
2487
|
+
await hubRequestJson(persisted.hubUrl, persisted.userToken, "/api/v1/hub/heartbeat", { method: "POST" });
|
|
2488
|
+
}
|
|
2489
|
+
return;
|
|
2490
|
+
}
|
|
2491
|
+
await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/heartbeat", { method: "POST" });
|
|
2492
|
+
} catch { /* best-effort */ }
|
|
2493
|
+
};
|
|
2494
|
+
sendHeartbeat();
|
|
2495
|
+
this.hubHeartbeatTimer = setInterval(sendHeartbeat, ViewerServer.HUB_HEARTBEAT_INTERVAL_MS);
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
private stopHubHeartbeat(): void {
|
|
2499
|
+
if (this.hubHeartbeatTimer) { clearInterval(this.hubHeartbeatTimer); this.hubHeartbeatTimer = undefined; }
|
|
2500
|
+
}
|
|
2501
|
+
|
|
1916
2502
|
private getLocalIPs(): string[] {
|
|
1917
2503
|
const nets = os.networkInterfaces();
|
|
1918
2504
|
const ips: string[] = [];
|
|
@@ -1965,7 +2551,7 @@ export class ViewerServer {
|
|
|
1965
2551
|
}
|
|
1966
2552
|
|
|
1967
2553
|
private handleSaveConfig(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
1968
|
-
this.readBody(req, (body) => {
|
|
2554
|
+
this.readBody(req, async (body) => {
|
|
1969
2555
|
try {
|
|
1970
2556
|
const newCfg = JSON.parse(body);
|
|
1971
2557
|
const cfgPath = this.getOpenClawConfigPath();
|
|
@@ -1988,6 +2574,11 @@ export class ViewerServer {
|
|
|
1988
2574
|
if (!entry.config) entry.config = {};
|
|
1989
2575
|
const config = entry.config as Record<string, unknown>;
|
|
1990
2576
|
|
|
2577
|
+
const oldSharing = config.sharing as Record<string, unknown> | undefined;
|
|
2578
|
+
const oldSharingRole = oldSharing?.role as string | undefined;
|
|
2579
|
+
const oldSharingEnabled = Boolean(oldSharing?.enabled);
|
|
2580
|
+
const oldClientHubAddress = String((oldSharing?.client as Record<string, unknown>)?.hubAddress || "");
|
|
2581
|
+
|
|
1991
2582
|
if (newCfg.embedding) config.embedding = newCfg.embedding;
|
|
1992
2583
|
if (newCfg.summarizer) config.summarizer = newCfg.summarizer;
|
|
1993
2584
|
if (newCfg.skillEvolution) config.skillEvolution = newCfg.skillEvolution;
|
|
@@ -2007,7 +2598,8 @@ export class ViewerServer {
|
|
|
2007
2598
|
localIPs.push("127.0.0.1", "localhost", "0.0.0.0");
|
|
2008
2599
|
try {
|
|
2009
2600
|
const u = new URL(addr.startsWith("http") ? addr : `http://${addr}`);
|
|
2010
|
-
|
|
2601
|
+
const targetPort = u.port || (u.protocol === "https:" ? "443" : "80");
|
|
2602
|
+
if (localIPs.includes(u.hostname) && targetPort === String(this.port)) {
|
|
2011
2603
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2012
2604
|
res.end(JSON.stringify({ error: "cannot_join_self" }));
|
|
2013
2605
|
return;
|
|
@@ -2015,6 +2607,43 @@ export class ViewerServer {
|
|
|
2015
2607
|
} catch {}
|
|
2016
2608
|
}
|
|
2017
2609
|
}
|
|
2610
|
+
|
|
2611
|
+
const newRole = merged.role as string | undefined;
|
|
2612
|
+
const newEnabled = Boolean(merged.enabled);
|
|
2613
|
+
|
|
2614
|
+
// Detect disabling sharing or switching away from hub mode
|
|
2615
|
+
const wasHub = oldSharingEnabled && oldSharingRole === "hub";
|
|
2616
|
+
const isHub = newEnabled && newRole === "hub";
|
|
2617
|
+
if (wasHub && !isHub) {
|
|
2618
|
+
await this.notifyHubShutdown();
|
|
2619
|
+
this.stopHubHeartbeat();
|
|
2620
|
+
this.log.info("Hub shutting down: notified connected clients");
|
|
2621
|
+
}
|
|
2622
|
+
|
|
2623
|
+
// Detect disabling sharing or switching away from client mode
|
|
2624
|
+
const wasClient = oldSharingEnabled && oldSharingRole === "client";
|
|
2625
|
+
const isClient = newEnabled && newRole === "client";
|
|
2626
|
+
if (wasClient && !isClient) {
|
|
2627
|
+
this.notifyHubLeave();
|
|
2628
|
+
const oldConn = this.store.getClientHubConnection();
|
|
2629
|
+
if (oldConn) {
|
|
2630
|
+
this.store.setClientHubConnection({ ...oldConn, userToken: "", lastKnownStatus: "left" });
|
|
2631
|
+
}
|
|
2632
|
+
this.log.info("Client hub connection token cleared (sharing disabled or role changed), identity preserved");
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
if (wasClient && isClient) {
|
|
2636
|
+
const newClientAddr = String((merged.client as Record<string, unknown>)?.hubAddress || "");
|
|
2637
|
+
if (newClientAddr && oldClientHubAddress && normalizeHubUrl(newClientAddr) !== normalizeHubUrl(oldClientHubAddress)) {
|
|
2638
|
+
this.notifyHubLeave();
|
|
2639
|
+
const oldConn = this.store.getClientHubConnection();
|
|
2640
|
+
if (oldConn) {
|
|
2641
|
+
this.store.setClientHubConnection({ ...oldConn, hubUrl: normalizeHubUrl(newClientAddr), userToken: "", lastKnownStatus: "hub_changed" });
|
|
2642
|
+
}
|
|
2643
|
+
this.log.info("Client hub connection token cleared (switched to different Hub), identity preserved");
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2018
2647
|
if (merged.role === "hub") {
|
|
2019
2648
|
merged.client = { hubAddress: "", userToken: "", teamToken: "" };
|
|
2020
2649
|
} else if (merged.role === "client") {
|
|
@@ -2026,6 +2655,16 @@ export class ViewerServer {
|
|
|
2026
2655
|
fs.mkdirSync(path.dirname(cfgPath), { recursive: true });
|
|
2027
2656
|
fs.writeFileSync(cfgPath, JSON.stringify(raw, null, 2), "utf-8");
|
|
2028
2657
|
this.log.info("Plugin config updated via Viewer");
|
|
2658
|
+
this.stopHubHeartbeat();
|
|
2659
|
+
|
|
2660
|
+
// When switching to client mode or re-enabling sharing as client, send join request
|
|
2661
|
+
const finalSharing = config.sharing as Record<string, unknown> | undefined;
|
|
2662
|
+
const nowClient = Boolean(finalSharing?.enabled) && finalSharing?.role === "client";
|
|
2663
|
+
const previouslyClient = oldSharingEnabled && oldSharingRole === "client";
|
|
2664
|
+
if (nowClient && !previouslyClient) {
|
|
2665
|
+
this.autoJoinOnSave(finalSharing).catch(e => this.log.warn(`Auto-join on save failed: ${e}`));
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2029
2668
|
this.jsonResponse(res, { ok: true });
|
|
2030
2669
|
} catch (e) {
|
|
2031
2670
|
this.log.warn(`handleSaveConfig error: ${e}`);
|
|
@@ -2035,6 +2674,92 @@ export class ViewerServer {
|
|
|
2035
2674
|
});
|
|
2036
2675
|
}
|
|
2037
2676
|
|
|
2677
|
+
private async autoJoinOnSave(sharing: Record<string, unknown>): Promise<void> {
|
|
2678
|
+
const clientCfg = sharing.client as Record<string, unknown> | undefined;
|
|
2679
|
+
const hubAddress = String(clientCfg?.hubAddress || "");
|
|
2680
|
+
const teamToken = String(clientCfg?.teamToken || "");
|
|
2681
|
+
if (!hubAddress || !teamToken) return;
|
|
2682
|
+
const hubUrl = normalizeHubUrl(hubAddress);
|
|
2683
|
+
const os = await import("os");
|
|
2684
|
+
const nickname = String(clientCfg?.nickname || "");
|
|
2685
|
+
const username = nickname || os.userInfo().username || "user";
|
|
2686
|
+
const hostname = os.hostname() || "unknown";
|
|
2687
|
+
const persisted = this.store.getClientHubConnection();
|
|
2688
|
+
const existingIdentityKey = persisted?.identityKey || "";
|
|
2689
|
+
const result = await hubRequestJson(hubUrl, "", "/api/v1/hub/join", {
|
|
2690
|
+
method: "POST",
|
|
2691
|
+
body: JSON.stringify({ teamToken, username, deviceName: hostname, identityKey: existingIdentityKey }),
|
|
2692
|
+
}) as any;
|
|
2693
|
+
const returnedIdentityKey = String(result.identityKey || existingIdentityKey || "");
|
|
2694
|
+
this.store.setClientHubConnection({
|
|
2695
|
+
hubUrl,
|
|
2696
|
+
userId: String(result.userId || ""),
|
|
2697
|
+
username,
|
|
2698
|
+
userToken: result.userToken || "",
|
|
2699
|
+
role: "member",
|
|
2700
|
+
connectedAt: Date.now(),
|
|
2701
|
+
identityKey: returnedIdentityKey,
|
|
2702
|
+
lastKnownStatus: result.status || "",
|
|
2703
|
+
});
|
|
2704
|
+
this.log.info(`Auto-join on save: status=${result.status}, userId=${result.userId}`);
|
|
2705
|
+
if (result.userToken) {
|
|
2706
|
+
this.startHubHeartbeat();
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
private async notifyHubLeave(): Promise<void> {
|
|
2711
|
+
try {
|
|
2712
|
+
const hub = this.resolveHubConnection();
|
|
2713
|
+
if (hub) {
|
|
2714
|
+
await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/leave", { method: "POST" });
|
|
2715
|
+
this.log.info("Notified Hub of voluntary leave");
|
|
2716
|
+
return;
|
|
2717
|
+
}
|
|
2718
|
+
const persisted = this.store.getClientHubConnection();
|
|
2719
|
+
if (persisted?.hubUrl && persisted?.userToken) {
|
|
2720
|
+
await hubRequestJson(persisted.hubUrl, persisted.userToken, "/api/v1/hub/leave", { method: "POST" });
|
|
2721
|
+
this.log.info("Notified Hub of voluntary leave (persisted connection)");
|
|
2722
|
+
}
|
|
2723
|
+
} catch (e) {
|
|
2724
|
+
this.log.warn(`Failed to notify Hub of leave: ${e}`);
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
|
|
2728
|
+
private async notifyHubShutdown(): Promise<void> {
|
|
2729
|
+
try {
|
|
2730
|
+
const sharing = this.ctx?.config.sharing;
|
|
2731
|
+
if (!sharing || sharing.role !== "hub") return;
|
|
2732
|
+
const hubPort = sharing.hub?.port ?? 18800;
|
|
2733
|
+
const authPath = path.join(this.dataDir, "hub-auth.json");
|
|
2734
|
+
let adminToken: string | undefined;
|
|
2735
|
+
try {
|
|
2736
|
+
const authData = JSON.parse(fs.readFileSync(authPath, "utf8"));
|
|
2737
|
+
adminToken = authData?.bootstrapAdminToken;
|
|
2738
|
+
} catch { return; }
|
|
2739
|
+
if (!adminToken) return;
|
|
2740
|
+
|
|
2741
|
+
const users = this.store.listHubUsers("active");
|
|
2742
|
+
const { v4: uuidv4 } = require("uuid");
|
|
2743
|
+
for (const u of users) {
|
|
2744
|
+
try {
|
|
2745
|
+
this.store.insertHubNotification({
|
|
2746
|
+
id: uuidv4(),
|
|
2747
|
+
userId: u.id,
|
|
2748
|
+
type: "hub_shutdown",
|
|
2749
|
+
resource: "hub",
|
|
2750
|
+
title: "Hub is shutting down",
|
|
2751
|
+
message: "The Hub server is shutting down. You may be disconnected.",
|
|
2752
|
+
});
|
|
2753
|
+
} catch (e) {
|
|
2754
|
+
this.log.warn(`Failed to insert shutdown notification for user ${u.id}: ${e}`);
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
this.log.info(`Hub shutdown: notified ${users.length} approved user(s)`);
|
|
2758
|
+
} catch (e) {
|
|
2759
|
+
this.log.warn(`notifyHubShutdown error: ${e}`);
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2038
2763
|
private handleUpdateUsername(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
2039
2764
|
this.readBody(req, async (body) => {
|
|
2040
2765
|
if (!this.ctx) return this.jsonResponse(res, { error: "sharing_unavailable" });
|
|
@@ -2062,10 +2787,10 @@ export class ViewerServer {
|
|
|
2062
2787
|
this.log.warn(`Failed to update hub-auth.json: ${e}`);
|
|
2063
2788
|
}
|
|
2064
2789
|
} else {
|
|
2065
|
-
const
|
|
2066
|
-
if (
|
|
2790
|
+
const persistedConn = this.store.getClientHubConnection();
|
|
2791
|
+
if (persistedConn) {
|
|
2067
2792
|
this.store.setClientHubConnection({
|
|
2068
|
-
...
|
|
2793
|
+
...persistedConn,
|
|
2069
2794
|
username: result.username,
|
|
2070
2795
|
userToken: result.userToken,
|
|
2071
2796
|
});
|
|
@@ -2092,7 +2817,8 @@ export class ViewerServer {
|
|
|
2092
2817
|
const localIPs = this.getLocalIPs();
|
|
2093
2818
|
localIPs.push("127.0.0.1", "localhost", "0.0.0.0");
|
|
2094
2819
|
const parsed = new URL(hubUrl.startsWith("http") ? hubUrl : `http://${hubUrl}`);
|
|
2095
|
-
|
|
2820
|
+
const targetPort = parsed.port || (parsed.protocol === "https:" ? "443" : "80");
|
|
2821
|
+
if (localIPs.includes(parsed.hostname) && targetPort === String(this.port)) {
|
|
2096
2822
|
this.jsonResponse(res, { ok: false, error: "cannot_join_self" });
|
|
2097
2823
|
return;
|
|
2098
2824
|
}
|
|
@@ -2488,7 +3214,7 @@ export class ViewerServer {
|
|
|
2488
3214
|
|
|
2489
3215
|
private getOpenClawHome(): string {
|
|
2490
3216
|
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
2491
|
-
return path.join(home, ".openclaw");
|
|
3217
|
+
return process.env.OPENCLAW_STATE_DIR || path.join(home, ".openclaw");
|
|
2492
3218
|
}
|
|
2493
3219
|
|
|
2494
3220
|
private handleCleanupPolluted(res: http.ServerResponse): void {
|
|
@@ -2517,7 +3243,7 @@ export class ViewerServer {
|
|
|
2517
3243
|
try {
|
|
2518
3244
|
const ocHome = this.getOpenClawHome();
|
|
2519
3245
|
const memoryDir = path.join(ocHome, "memory");
|
|
2520
|
-
const
|
|
3246
|
+
const agentsDir = path.join(ocHome, "agents");
|
|
2521
3247
|
|
|
2522
3248
|
const sqliteFiles: Array<{ file: string; chunks: number }> = [];
|
|
2523
3249
|
if (fs.existsSync(memoryDir)) {
|
|
@@ -2536,31 +3262,36 @@ export class ViewerServer {
|
|
|
2536
3262
|
|
|
2537
3263
|
let sessionCount = 0;
|
|
2538
3264
|
let messageCount = 0;
|
|
2539
|
-
if (fs.existsSync(
|
|
2540
|
-
const
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
3265
|
+
if (fs.existsSync(agentsDir)) {
|
|
3266
|
+
for (const entry of fs.readdirSync(agentsDir, { withFileTypes: true })) {
|
|
3267
|
+
if (!entry.isDirectory()) continue;
|
|
3268
|
+
const sessDir = path.join(agentsDir, entry.name, "sessions");
|
|
3269
|
+
if (!fs.existsSync(sessDir)) continue;
|
|
3270
|
+
const jsonlFiles = fs.readdirSync(sessDir).filter(f => f.includes(".jsonl"));
|
|
3271
|
+
sessionCount += jsonlFiles.length;
|
|
3272
|
+
for (const f of jsonlFiles) {
|
|
3273
|
+
try {
|
|
3274
|
+
const content = fs.readFileSync(path.join(sessDir, f), "utf-8");
|
|
3275
|
+
const lines = content.split("\n").filter(l => l.trim());
|
|
3276
|
+
for (const line of lines) {
|
|
3277
|
+
try {
|
|
3278
|
+
const obj = JSON.parse(line);
|
|
3279
|
+
if (obj.type === "message") {
|
|
3280
|
+
const role = obj.message?.role ?? obj.role;
|
|
3281
|
+
if (role === "user" || role === "assistant") {
|
|
3282
|
+
const mc = obj.message?.content ?? obj.content;
|
|
3283
|
+
let txt = "";
|
|
3284
|
+
if (typeof mc === "string") txt = mc;
|
|
3285
|
+
else if (Array.isArray(mc)) txt = mc.filter((p: any) => p.type === "text" && p.text).map((p: any) => p.text).join("\n");
|
|
3286
|
+
else txt = JSON.stringify(mc);
|
|
3287
|
+
if (role === "user") txt = stripInboundMetadata(txt);
|
|
3288
|
+
if (txt && txt.length >= 10) messageCount++;
|
|
3289
|
+
}
|
|
2559
3290
|
}
|
|
2560
|
-
}
|
|
2561
|
-
}
|
|
2562
|
-
}
|
|
2563
|
-
}
|
|
3291
|
+
} catch { /* skip bad lines */ }
|
|
3292
|
+
}
|
|
3293
|
+
} catch { /* skip unreadable */ }
|
|
3294
|
+
}
|
|
2564
3295
|
}
|
|
2565
3296
|
}
|
|
2566
3297
|
|