@memtensor/memos-local-openclaw-plugin 1.0.4-beta.0 → 1.0.4-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +7 -0
- package/README.md +24 -24
- package/dist/capture/index.d.ts +1 -1
- package/dist/capture/index.d.ts.map +1 -1
- package/dist/capture/index.js +34 -2
- package/dist/capture/index.js.map +1 -1
- package/dist/client/connector.d.ts +5 -2
- package/dist/client/connector.d.ts.map +1 -1
- package/dist/client/connector.js +173 -14
- package/dist/client/connector.js.map +1 -1
- package/dist/client/hub.d.ts.map +1 -1
- package/dist/client/hub.js +22 -0
- package/dist/client/hub.js.map +1 -1
- package/dist/client/skill-sync.d.ts +7 -0
- package/dist/client/skill-sync.d.ts.map +1 -1
- package/dist/client/skill-sync.js +10 -0
- package/dist/client/skill-sync.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +9 -11
- package/dist/config.js.map +1 -1
- package/dist/hub/server.d.ts +7 -0
- package/dist/hub/server.d.ts.map +1 -1
- package/dist/hub/server.js +301 -106
- package/dist/hub/server.js.map +1 -1
- package/dist/hub/user-manager.d.ts +3 -0
- package/dist/hub/user-manager.d.ts.map +1 -1
- package/dist/hub/user-manager.js +18 -1
- package/dist/hub/user-manager.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -2
- package/dist/index.js.map +1 -1
- package/dist/ingest/providers/index.d.ts.map +1 -1
- package/dist/ingest/providers/index.js +37 -6
- package/dist/ingest/providers/index.js.map +1 -1
- package/dist/recall/engine.d.ts.map +1 -1
- package/dist/recall/engine.js +91 -1
- package/dist/recall/engine.js.map +1 -1
- package/dist/shared/llm-call.d.ts +1 -0
- package/dist/shared/llm-call.d.ts.map +1 -1
- package/dist/shared/llm-call.js +82 -8
- package/dist/shared/llm-call.js.map +1 -1
- package/dist/sharing/types.d.ts +1 -1
- package/dist/sharing/types.d.ts.map +1 -1
- package/dist/skill/evolver.d.ts +2 -0
- package/dist/skill/evolver.d.ts.map +1 -1
- package/dist/skill/evolver.js +3 -0
- package/dist/skill/evolver.js.map +1 -1
- package/dist/storage/ensure-binding.d.ts +12 -0
- package/dist/storage/ensure-binding.d.ts.map +1 -0
- package/dist/storage/ensure-binding.js +53 -0
- package/dist/storage/ensure-binding.js.map +1 -0
- package/dist/storage/sqlite.d.ts +74 -20
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +301 -207
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/telemetry.d.ts +12 -5
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +156 -40
- package/dist/telemetry.js.map +1 -1
- package/dist/tools/memory-search.d.ts +3 -1
- package/dist/tools/memory-search.d.ts.map +1 -1
- package/dist/tools/memory-search.js +3 -1
- package/dist/tools/memory-search.js.map +1 -1
- package/dist/types.d.ts +1 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/viewer/html.d.ts.map +1 -1
- package/dist/viewer/html.js +2991 -1041
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +32 -8
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +1122 -261
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +384 -43
- package/openclaw.plugin.json +1 -1
- package/package.json +3 -2
- package/scripts/postinstall.cjs +1 -1
- package/skill/memos-memory-guide/SKILL.md +64 -26
- package/src/capture/index.ts +37 -1
- package/src/client/connector.ts +173 -16
- package/src/client/hub.ts +18 -0
- package/src/client/skill-sync.ts +14 -0
- package/src/config.ts +9 -11
- package/src/hub/server.ts +285 -98
- package/src/hub/user-manager.ts +20 -3
- package/src/index.ts +10 -2
- package/src/ingest/providers/index.ts +41 -7
- package/src/recall/engine.ts +84 -1
- package/src/shared/llm-call.ts +97 -9
- package/src/sharing/types.ts +1 -1
- package/src/skill/evolver.ts +5 -0
- package/src/storage/ensure-binding.ts +52 -0
- package/src/storage/sqlite.ts +310 -233
- package/src/telemetry.ts +172 -41
- package/src/tools/memory-search.ts +2 -1
- package/src/types.ts +1 -2
- package/src/viewer/html.ts +2991 -1041
- package/src/viewer/server.ts +984 -190
package/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,9 @@ 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);
|
|
272
|
+
else if (p === "/api/sharing/retry-join" && req.method === "POST") this.handleRetryJoin(req, res);
|
|
254
273
|
else if (p === "/api/sharing/search/memories" && req.method === "POST") this.handleSharingMemorySearch(req, res);
|
|
255
274
|
else if (p === "/api/sharing/memories/list" && req.method === "GET") this.serveSharingMemoryList(res, url);
|
|
256
275
|
else if (p === "/api/sharing/tasks/list" && req.method === "GET") this.serveSharingTaskList(res, url);
|
|
@@ -260,23 +279,23 @@ export class ViewerServer {
|
|
|
260
279
|
else if (p === "/api/sharing/tasks/share" && req.method === "POST") this.handleSharingTaskShare(req, res);
|
|
261
280
|
else if (p === "/api/sharing/tasks/unshare" && req.method === "POST") this.handleSharingTaskUnshare(req, res);
|
|
262
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);
|
|
263
283
|
else if (p === "/api/sharing/test-hub" && req.method === "POST") this.handleTestHubConnection(req, res);
|
|
264
284
|
else if (p === "/api/sharing/memories/share" && req.method === "POST") this.handleSharingMemoryShare(req, res);
|
|
265
285
|
else if (p === "/api/sharing/memories/unshare" && req.method === "POST") this.handleSharingMemoryUnshare(req, res);
|
|
266
286
|
else if (p === "/api/sharing/skills/pull" && req.method === "POST") this.handleSharingSkillPull(req, res);
|
|
267
287
|
else if (p === "/api/sharing/skills/share" && req.method === "POST") this.handleSharingSkillShare(req, res);
|
|
268
288
|
else if (p === "/api/sharing/skills/unshare" && req.method === "POST") this.handleSharingSkillUnshare(req, res);
|
|
269
|
-
else if (p === "/api/sharing/groups" && req.method === "GET") this.serveSharingGroups(res);
|
|
270
|
-
else if (p === "/api/sharing/groups" && req.method === "POST") this.handleSharingGroupCreate(req, res);
|
|
271
|
-
else if (p.match(/^\/api\/sharing\/groups\/[^/]+$/) && req.method === "PUT") this.handleSharingGroupUpdate(req, res, p);
|
|
272
|
-
else if (p.match(/^\/api\/sharing\/groups\/[^/]+$/) && req.method === "DELETE") this.handleSharingGroupDelete(res, p);
|
|
273
|
-
else if (p.match(/^\/api\/sharing\/groups\/[^/]+\/members$/) && req.method === "GET") this.serveSharingGroupMembers(res, p);
|
|
274
|
-
else if (p.match(/^\/api\/sharing\/groups\/[^/]+\/members$/) && req.method === "POST") this.handleSharingGroupAddMember(req, res, p);
|
|
275
|
-
else if (p.match(/^\/api\/sharing\/groups\/[^/]+\/members$/) && req.method === "DELETE") this.handleSharingGroupRemoveMember(req, res, p);
|
|
276
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);
|
|
277
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);
|
|
278
296
|
else if (p.match(/^\/api\/admin\/shared-tasks\/[^/]+$/) && req.method === "DELETE") this.handleAdminDeleteTask(res, p);
|
|
279
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);
|
|
280
299
|
else if (p.match(/^\/api\/admin\/shared-skills\/[^/]+$/) && req.method === "DELETE") this.handleAdminDeleteSkill(res, p);
|
|
281
300
|
else if (p === "/api/admin/shared-memories" && req.method === "GET") this.serveAdminSharedMemories(res);
|
|
282
301
|
else if (p.match(/^\/api\/admin\/shared-memories\/[^/]+$/) && req.method === "DELETE") this.handleAdminDeleteMemory(res, p);
|
|
@@ -431,22 +450,30 @@ export class ViewerServer {
|
|
|
431
450
|
const params: any[] = [];
|
|
432
451
|
if (session) { conditions.push("session_key = ?"); params.push(session); }
|
|
433
452
|
if (role) { conditions.push("role = ?"); params.push(role); }
|
|
434
|
-
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
|
+
}
|
|
435
459
|
if (dateFrom) { conditions.push("created_at >= ?"); params.push(new Date(dateFrom).getTime()); }
|
|
436
460
|
if (dateTo) { conditions.push("created_at <= ?"); params.push(new Date(dateTo).getTime()); }
|
|
437
461
|
|
|
438
462
|
const where = conditions.length > 0 ? " WHERE " + conditions.join(" AND ") : "";
|
|
439
463
|
const totalRow = db.prepare("SELECT COUNT(*) as count FROM chunks" + where).get(...params) as any;
|
|
440
|
-
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);
|
|
441
465
|
const findMergeSources = db.prepare("SELECT id, summary, role FROM chunks WHERE dedup_target = ? AND (dedup_status = 'merged' OR dedup_status = 'duplicate')");
|
|
442
466
|
|
|
443
467
|
const chunkIds = rawMemories.map((m: any) => m.id);
|
|
444
468
|
const sharingMap = new Map<string, { visibility: string; group_id: string | null }>();
|
|
469
|
+
const localShareMap = new Map<string, { original_owner: string; shared_at: number }>();
|
|
445
470
|
if (chunkIds.length > 0) {
|
|
446
471
|
try {
|
|
447
472
|
const placeholders = chunkIds.map(() => "?").join(",");
|
|
448
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 }>;
|
|
449
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);
|
|
450
477
|
} catch {
|
|
451
478
|
}
|
|
452
479
|
}
|
|
@@ -457,8 +484,12 @@ export class ViewerServer {
|
|
|
457
484
|
out.merge_sources = sources;
|
|
458
485
|
}
|
|
459
486
|
const shared = sharingMap.get(m.id);
|
|
487
|
+
const localShared = localShareMap.get(m.id);
|
|
460
488
|
out.sharingVisibility = shared?.visibility ?? null;
|
|
461
489
|
out.sharingGroupId = shared?.group_id ?? null;
|
|
490
|
+
out.localSharing = out.owner === "public";
|
|
491
|
+
out.localSharingManaged = !!localShared;
|
|
492
|
+
out.localOriginalOwner = localShared?.original_owner ?? null;
|
|
462
493
|
return out;
|
|
463
494
|
});
|
|
464
495
|
|
|
@@ -476,7 +507,21 @@ export class ViewerServer {
|
|
|
476
507
|
}
|
|
477
508
|
|
|
478
509
|
private serveToolMetrics(res: http.ServerResponse, url: URL): void {
|
|
479
|
-
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));
|
|
480
525
|
const data = this.store.getToolMetrics(minutes);
|
|
481
526
|
this.jsonResponse(res, data);
|
|
482
527
|
}
|
|
@@ -484,13 +529,15 @@ export class ViewerServer {
|
|
|
484
529
|
private serveTasks(res: http.ServerResponse, url: URL): void {
|
|
485
530
|
this.store.recordViewerEvent("tasks_list");
|
|
486
531
|
const status = url.searchParams.get("status") ?? undefined;
|
|
532
|
+
const owner = url.searchParams.get("owner") ?? undefined;
|
|
487
533
|
const limit = Math.min(100, Math.max(1, Number(url.searchParams.get("limit")) || 50));
|
|
488
534
|
const offset = Math.max(0, Number(url.searchParams.get("offset")) || 0);
|
|
489
|
-
const { tasks, total } = this.store.listTasks({ status, limit, offset });
|
|
535
|
+
const { tasks, total } = this.store.listTasks({ status, limit, offset, owner });
|
|
490
536
|
|
|
491
537
|
const db = (this.store as any).db;
|
|
492
538
|
const items = tasks.map((t) => {
|
|
493
|
-
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;
|
|
494
541
|
return {
|
|
495
542
|
id: t.id,
|
|
496
543
|
sessionKey: t.sessionKey,
|
|
@@ -501,6 +548,8 @@ export class ViewerServer {
|
|
|
501
548
|
endedAt: t.endedAt,
|
|
502
549
|
chunkCount: this.store.countChunksByTask(t.id),
|
|
503
550
|
skillStatus: meta?.skill_status ?? null,
|
|
551
|
+
owner: meta?.owner ?? "agent:main",
|
|
552
|
+
sharingVisibility: sharedTask?.visibility ?? null,
|
|
504
553
|
};
|
|
505
554
|
});
|
|
506
555
|
|
|
@@ -543,6 +592,7 @@ export class ViewerServer {
|
|
|
543
592
|
title: task.title,
|
|
544
593
|
summary: task.summary,
|
|
545
594
|
status: task.status,
|
|
595
|
+
owner: task.owner ?? "agent:main",
|
|
546
596
|
startedAt: task.startedAt,
|
|
547
597
|
endedAt: task.endedAt,
|
|
548
598
|
chunks: chunkItems,
|
|
@@ -551,6 +601,7 @@ export class ViewerServer {
|
|
|
551
601
|
skillLinks,
|
|
552
602
|
sharingVisibility: sharedTask?.visibility ?? null,
|
|
553
603
|
sharingGroupId: sharedTask?.group_id ?? null,
|
|
604
|
+
hubTaskId: sharedTask ? true : false,
|
|
554
605
|
});
|
|
555
606
|
}
|
|
556
607
|
|
|
@@ -585,12 +636,19 @@ export class ViewerServer {
|
|
|
585
636
|
}
|
|
586
637
|
let embCount = 0;
|
|
587
638
|
try { embCount = (db.prepare("SELECT COUNT(*) as count FROM embeddings").get() as any).count; } catch { /* table may not exist */ }
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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[];
|
|
594
652
|
|
|
595
653
|
let skillCount = 0;
|
|
596
654
|
try { skillCount = (db.prepare("SELECT COUNT(*) as count FROM skills").get() as any).count; } catch { /* table may not exist yet */ }
|
|
@@ -603,10 +661,16 @@ export class ViewerServer {
|
|
|
603
661
|
|
|
604
662
|
let owners: string[] = [];
|
|
605
663
|
try {
|
|
606
|
-
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[];
|
|
607
665
|
owners = ownerRows.map((o: any) => o.owner);
|
|
608
666
|
} catch { /* column may not exist yet */ }
|
|
609
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
|
+
|
|
610
674
|
this.jsonResponse(res, {
|
|
611
675
|
totalMemories: total.count, totalSessions: sessions.count, totalEmbeddings: embCount,
|
|
612
676
|
totalSkills: skillCount,
|
|
@@ -615,6 +679,7 @@ export class ViewerServer {
|
|
|
615
679
|
timeRange: { earliest: timeRange.earliest, latest: timeRange.latest },
|
|
616
680
|
sessions: sessionList,
|
|
617
681
|
owners,
|
|
682
|
+
currentAgentOwner,
|
|
618
683
|
});
|
|
619
684
|
} catch (e) {
|
|
620
685
|
this.log.warn(`stats error: ${e}`);
|
|
@@ -726,7 +791,12 @@ export class ViewerServer {
|
|
|
726
791
|
if (visibility) {
|
|
727
792
|
skills = skills.filter(s => s.visibility === visibility);
|
|
728
793
|
}
|
|
729
|
-
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 });
|
|
730
800
|
}
|
|
731
801
|
|
|
732
802
|
private serveSkillDetail(res: http.ServerResponse, urlPath: string): void {
|
|
@@ -977,11 +1047,21 @@ export class ViewerServer {
|
|
|
977
1047
|
});
|
|
978
1048
|
}
|
|
979
1049
|
|
|
980
|
-
private handleSkillDelete(res: http.ServerResponse, urlPath: string): void {
|
|
1050
|
+
private async handleSkillDelete(res: http.ServerResponse, urlPath: string): Promise<void> {
|
|
981
1051
|
const skillId = urlPath.replace("/api/skill/", "");
|
|
982
1052
|
const skill = this.store.getSkill(skillId);
|
|
983
1053
|
if (!skill) { res.writeHead(404, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Skill not found" })); return; }
|
|
984
|
-
|
|
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 (_) {}
|
|
985
1065
|
try {
|
|
986
1066
|
if (skill.dirPath && fs.existsSync(skill.dirPath)) {
|
|
987
1067
|
fs.rmSync(skill.dirPath, { recursive: true, force: true });
|
|
@@ -1028,7 +1108,15 @@ export class ViewerServer {
|
|
|
1028
1108
|
const cleaned = chunk.role === "user" && chunk.content
|
|
1029
1109
|
? { ...chunk, content: stripInboundMetadata(chunk.content) }
|
|
1030
1110
|
: chunk;
|
|
1031
|
-
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
|
+
});
|
|
1032
1120
|
}
|
|
1033
1121
|
|
|
1034
1122
|
private handleUpdate(req: http.IncomingMessage, res: http.ServerResponse, urlPath: string): void {
|
|
@@ -1057,6 +1145,349 @@ export class ViewerServer {
|
|
|
1057
1145
|
else { res.writeHead(404, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Not found" })); }
|
|
1058
1146
|
}
|
|
1059
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
|
+
|
|
1060
1491
|
private handleDeleteSession(res: http.ServerResponse, url: URL): void {
|
|
1061
1492
|
const key = url.searchParams.get("key");
|
|
1062
1493
|
if (!key) { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Missing key" })); return; }
|
|
@@ -1092,7 +1523,8 @@ export class ViewerServer {
|
|
|
1092
1523
|
|
|
1093
1524
|
private getOpenClawConfigPath(): string {
|
|
1094
1525
|
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
1095
|
-
|
|
1526
|
+
const ocHome = process.env.OPENCLAW_STATE_DIR || path.join(home, ".openclaw");
|
|
1527
|
+
return path.join(ocHome, "openclaw.json");
|
|
1096
1528
|
}
|
|
1097
1529
|
|
|
1098
1530
|
private getPluginEntryConfig(raw: any): Record<string, unknown> {
|
|
@@ -1156,8 +1588,7 @@ export class ViewerServer {
|
|
|
1156
1588
|
base.connection.connected = true;
|
|
1157
1589
|
base.connection.hubUrl = resolvedHubUrl ?? undefined;
|
|
1158
1590
|
|
|
1159
|
-
|
|
1160
|
-
let adminUser: any = { username: "hub-admin", role: "admin", groups: [] };
|
|
1591
|
+
let adminUser: any = { username: "hub-admin", role: "admin" };
|
|
1161
1592
|
try {
|
|
1162
1593
|
const hub = this.resolveHubConnection();
|
|
1163
1594
|
if (hub) {
|
|
@@ -1167,7 +1598,6 @@ export class ViewerServer {
|
|
|
1167
1598
|
id: me.id,
|
|
1168
1599
|
username: me.username ?? "hub-admin",
|
|
1169
1600
|
role: me.role ?? "admin",
|
|
1170
|
-
groups: Array.isArray(me.groups) ? me.groups : [],
|
|
1171
1601
|
};
|
|
1172
1602
|
}
|
|
1173
1603
|
}
|
|
@@ -1185,7 +1615,8 @@ export class ViewerServer {
|
|
|
1185
1615
|
return;
|
|
1186
1616
|
}
|
|
1187
1617
|
|
|
1188
|
-
|
|
1618
|
+
const hasPendingConnection = Boolean(persisted?.hubUrl && persisted?.userId && !persisted?.userToken);
|
|
1619
|
+
if (!hasClientConfig && !hasPendingConnection) {
|
|
1189
1620
|
this.jsonResponse(res, base);
|
|
1190
1621
|
return;
|
|
1191
1622
|
}
|
|
@@ -1193,12 +1624,26 @@ export class ViewerServer {
|
|
|
1193
1624
|
try {
|
|
1194
1625
|
const status = await getHubStatus(this.store, this.ctx.config);
|
|
1195
1626
|
const output = { ...base, connection: { ...base.connection, ...status } } as any;
|
|
1627
|
+
if (status.user?.status === "pending") {
|
|
1628
|
+
output.connection.pendingApproval = true;
|
|
1629
|
+
}
|
|
1630
|
+
if (status.user?.status === "rejected") {
|
|
1631
|
+
output.connection.rejected = true;
|
|
1632
|
+
}
|
|
1633
|
+
if (status.user?.status === "removed") {
|
|
1634
|
+
output.connection.removed = true;
|
|
1635
|
+
}
|
|
1196
1636
|
if (status.connected && status.hubUrl) {
|
|
1197
1637
|
try {
|
|
1198
1638
|
const info = await fetch(`${status.hubUrl}/api/v1/hub/info`).then((r) => (r.ok ? r.json() : null)).catch(() => null) as any;
|
|
1199
1639
|
output.connection.teamName = info?.teamName ?? null;
|
|
1200
1640
|
output.connection.apiVersion = info?.apiVersion ?? null;
|
|
1201
1641
|
} catch {}
|
|
1642
|
+
} else if (status.hubUrl) {
|
|
1643
|
+
try {
|
|
1644
|
+
const info = await fetch(`${status.hubUrl}/api/v1/hub/info`).then((r) => (r.ok ? r.json() : null)).catch(() => null) as any;
|
|
1645
|
+
output.connection.teamName = info?.teamName ?? null;
|
|
1646
|
+
} catch {}
|
|
1202
1647
|
}
|
|
1203
1648
|
output.admin.canManageUsers = status.connected && status.user?.role === "admin";
|
|
1204
1649
|
output.admin.rejectSupported = output.admin.canManageUsers;
|
|
@@ -1256,11 +1701,123 @@ export class ViewerServer {
|
|
|
1256
1701
|
});
|
|
1257
1702
|
}
|
|
1258
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
|
+
|
|
1765
|
+
private handleRetryJoin(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
1766
|
+
this.readBody(req, async (_body) => {
|
|
1767
|
+
if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
1768
|
+
const sharing = this.ctx.config.sharing;
|
|
1769
|
+
if (!sharing?.enabled || sharing.role !== "client") {
|
|
1770
|
+
return this.jsonResponse(res, { ok: false, error: "not_in_client_mode" });
|
|
1771
|
+
}
|
|
1772
|
+
const hubAddress = sharing.client?.hubAddress ?? "";
|
|
1773
|
+
const teamToken = sharing.client?.teamToken ?? "";
|
|
1774
|
+
if (!hubAddress || !teamToken) {
|
|
1775
|
+
return this.jsonResponse(res, { ok: false, error: "missing_hub_address_or_team_token" });
|
|
1776
|
+
}
|
|
1777
|
+
try {
|
|
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
|
+
if (localIPs.includes(u.hostname)) {
|
|
1784
|
+
return this.jsonResponse(res, { ok: false, error: "cannot_join_self" });
|
|
1785
|
+
}
|
|
1786
|
+
} catch {}
|
|
1787
|
+
const os = await import("os");
|
|
1788
|
+
const nickname = sharing.client?.nickname;
|
|
1789
|
+
const username = nickname || os.userInfo().username || "user";
|
|
1790
|
+
const hostname = os.hostname() || "unknown";
|
|
1791
|
+
const result = await hubRequestJson(hubUrl, "", "/api/v1/hub/join", {
|
|
1792
|
+
method: "POST",
|
|
1793
|
+
body: JSON.stringify({ teamToken, username, deviceName: hostname, reapply: true }),
|
|
1794
|
+
}) as any;
|
|
1795
|
+
this.store.setClientHubConnection({
|
|
1796
|
+
hubUrl,
|
|
1797
|
+
userId: String(result.userId || ""),
|
|
1798
|
+
username,
|
|
1799
|
+
userToken: result.userToken || "",
|
|
1800
|
+
role: "member",
|
|
1801
|
+
connectedAt: Date.now(),
|
|
1802
|
+
});
|
|
1803
|
+
this.jsonResponse(res, { ok: true, status: result.status || "pending" });
|
|
1804
|
+
} catch (err) {
|
|
1805
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1806
|
+
}
|
|
1807
|
+
});
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1259
1810
|
private async serveSharingMemoryList(res: http.ServerResponse, url: URL): Promise<void> {
|
|
1260
1811
|
if (!this.ctx) return this.jsonResponse(res, { memories: [], error: "sharing_unavailable" });
|
|
1261
1812
|
try {
|
|
1262
1813
|
const limit = Number(url.searchParams.get("limit") || 40);
|
|
1263
|
-
const
|
|
1814
|
+
const hub = this.resolveHubConnection();
|
|
1815
|
+
let data: any;
|
|
1816
|
+
if (hub) {
|
|
1817
|
+
data = await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/memories?limit=${limit}`);
|
|
1818
|
+
} else {
|
|
1819
|
+
data = await hubListMemories(this.store, this.ctx, { limit });
|
|
1820
|
+
}
|
|
1264
1821
|
this.jsonResponse(res, { memories: Array.isArray(data?.memories) ? data.memories : [] });
|
|
1265
1822
|
} catch (err) {
|
|
1266
1823
|
this.jsonResponse(res, { memories: [], error: String(err) });
|
|
@@ -1271,7 +1828,13 @@ export class ViewerServer {
|
|
|
1271
1828
|
if (!this.ctx) return this.jsonResponse(res, { tasks: [], error: "sharing_unavailable" });
|
|
1272
1829
|
try {
|
|
1273
1830
|
const limit = Number(url.searchParams.get("limit") || 40);
|
|
1274
|
-
const
|
|
1831
|
+
const hub = this.resolveHubConnection();
|
|
1832
|
+
let data: any;
|
|
1833
|
+
if (hub) {
|
|
1834
|
+
data = await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/tasks?limit=${limit}`);
|
|
1835
|
+
} else {
|
|
1836
|
+
data = await hubListTasks(this.store, this.ctx, { limit });
|
|
1837
|
+
}
|
|
1275
1838
|
this.jsonResponse(res, { tasks: Array.isArray(data?.tasks) ? data.tasks : [] });
|
|
1276
1839
|
} catch (err) {
|
|
1277
1840
|
this.jsonResponse(res, { tasks: [], error: String(err) });
|
|
@@ -1282,7 +1845,13 @@ export class ViewerServer {
|
|
|
1282
1845
|
if (!this.ctx) return this.jsonResponse(res, { skills: [], error: "sharing_unavailable" });
|
|
1283
1846
|
try {
|
|
1284
1847
|
const limit = Number(url.searchParams.get("limit") || 40);
|
|
1285
|
-
const
|
|
1848
|
+
const hub = this.resolveHubConnection();
|
|
1849
|
+
let data: any;
|
|
1850
|
+
if (hub) {
|
|
1851
|
+
data = await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/skills/list?limit=${limit}`);
|
|
1852
|
+
} else {
|
|
1853
|
+
data = await hubListSkills(this.store, this.ctx, { limit });
|
|
1854
|
+
}
|
|
1286
1855
|
this.jsonResponse(res, { skills: Array.isArray(data?.skills) ? data.skills : [] });
|
|
1287
1856
|
} catch (err) {
|
|
1288
1857
|
this.jsonResponse(res, { skills: [], error: String(err) });
|
|
@@ -1298,13 +1867,21 @@ export class ViewerServer {
|
|
|
1298
1867
|
const query = String(parsed.query || "");
|
|
1299
1868
|
const role = typeof parsed.role === "string" ? parsed.role : undefined;
|
|
1300
1869
|
const maxResults = typeof parsed.maxResults === "number" ? parsed.maxResults : 10;
|
|
1301
|
-
const scope = parsed.scope === "group" || parsed.scope === "all" ? parsed.scope : "local";
|
|
1870
|
+
const scope = parsed.scope === "group" || parsed.scope === "all" || parsed.scope === "hub" ? (parsed.scope === "hub" ? "all" : parsed.scope) : "local";
|
|
1302
1871
|
const local = this.searchLocalViewerMemories(query, { role, maxResults });
|
|
1303
1872
|
if (scope === "local") {
|
|
1304
1873
|
return this.jsonResponse(res, { local: { hits: local.hits, meta: local.meta }, hub: emptyHub });
|
|
1305
1874
|
}
|
|
1306
1875
|
try {
|
|
1307
|
-
const
|
|
1876
|
+
const conn = this.resolveHubConnection();
|
|
1877
|
+
let hub: any;
|
|
1878
|
+
if (conn) {
|
|
1879
|
+
hub = await hubRequestJson(conn.hubUrl, conn.userToken, "/api/v1/hub/search", {
|
|
1880
|
+
method: "POST", body: JSON.stringify({ query, maxResults, scope }),
|
|
1881
|
+
});
|
|
1882
|
+
} else {
|
|
1883
|
+
hub = await hubSearchMemories(this.store, this.ctx!, { query, maxResults, scope });
|
|
1884
|
+
}
|
|
1308
1885
|
this.jsonResponse(res, { local: { hits: local.hits, meta: local.meta }, hub });
|
|
1309
1886
|
} catch (err) {
|
|
1310
1887
|
this.jsonResponse(res, { local: { hits: local.hits, meta: local.meta }, hub: emptyHub, error: String(err) });
|
|
@@ -1395,8 +1972,8 @@ export class ViewerServer {
|
|
|
1395
1972
|
try {
|
|
1396
1973
|
const parsed = JSON.parse(body || "{}");
|
|
1397
1974
|
const taskId = String(parsed.taskId || "");
|
|
1398
|
-
const visibility =
|
|
1399
|
-
const groupId
|
|
1975
|
+
const visibility = "public";
|
|
1976
|
+
const groupId: string | undefined = undefined;
|
|
1400
1977
|
const task = this.store.getTask(taskId);
|
|
1401
1978
|
if (!task) return this.jsonResponse(res, { ok: false, error: "task_not_found" });
|
|
1402
1979
|
const chunks = this.store.getChunksByTask(taskId);
|
|
@@ -1410,7 +1987,7 @@ export class ViewerServer {
|
|
|
1410
1987
|
sourceTaskId: task.id,
|
|
1411
1988
|
title: task.title,
|
|
1412
1989
|
summary: task.summary,
|
|
1413
|
-
groupId:
|
|
1990
|
+
groupId: null,
|
|
1414
1991
|
visibility,
|
|
1415
1992
|
createdAt: task.startedAt ?? Date.now(),
|
|
1416
1993
|
updatedAt: task.updatedAt ?? Date.now(),
|
|
@@ -1436,7 +2013,7 @@ export class ViewerServer {
|
|
|
1436
2013
|
sourceUserId: hubUserId,
|
|
1437
2014
|
title: task.title,
|
|
1438
2015
|
summary: task.summary,
|
|
1439
|
-
groupId:
|
|
2016
|
+
groupId: null,
|
|
1440
2017
|
visibility,
|
|
1441
2018
|
createdAt: task.startedAt ?? Date.now(),
|
|
1442
2019
|
updatedAt: task.updatedAt ?? Date.now(),
|
|
@@ -1477,8 +2054,8 @@ export class ViewerServer {
|
|
|
1477
2054
|
try {
|
|
1478
2055
|
const parsed = JSON.parse(body || "{}");
|
|
1479
2056
|
const chunkId = String(parsed.chunkId || "");
|
|
1480
|
-
const visibility =
|
|
1481
|
-
const groupId
|
|
2057
|
+
const visibility = "public";
|
|
2058
|
+
const groupId: string | undefined = undefined;
|
|
1482
2059
|
const db = (this.store as any).db;
|
|
1483
2060
|
const chunk = db.prepare("SELECT * FROM chunks WHERE id = ?").get(chunkId) as any;
|
|
1484
2061
|
if (!chunk) return this.jsonResponse(res, { ok: false, error: "memory_not_found" });
|
|
@@ -1492,7 +2069,7 @@ export class ViewerServer {
|
|
|
1492
2069
|
content: chunk.content,
|
|
1493
2070
|
summary: chunk.summary,
|
|
1494
2071
|
kind: chunk.kind,
|
|
1495
|
-
groupId:
|
|
2072
|
+
groupId: null,
|
|
1496
2073
|
visibility,
|
|
1497
2074
|
},
|
|
1498
2075
|
}),
|
|
@@ -1509,7 +2086,7 @@ export class ViewerServer {
|
|
|
1509
2086
|
content: chunk.content,
|
|
1510
2087
|
summary: chunk.summary ?? "",
|
|
1511
2088
|
kind: chunk.kind,
|
|
1512
|
-
groupId:
|
|
2089
|
+
groupId: null,
|
|
1513
2090
|
visibility,
|
|
1514
2091
|
createdAt: existing?.createdAt ?? now,
|
|
1515
2092
|
updatedAt: now,
|
|
@@ -1563,8 +2140,8 @@ export class ViewerServer {
|
|
|
1563
2140
|
try {
|
|
1564
2141
|
const parsed = JSON.parse(body || "{}");
|
|
1565
2142
|
const skillId = String(parsed.skillId || "");
|
|
1566
|
-
const visibility =
|
|
1567
|
-
const groupId
|
|
2143
|
+
const visibility = "public";
|
|
2144
|
+
const groupId: string | null = null;
|
|
1568
2145
|
const skill = this.store.getSkill(skillId);
|
|
1569
2146
|
if (!skill) return this.jsonResponse(res, { ok: false, error: "skill_not_found" });
|
|
1570
2147
|
const bundle = buildSkillBundleForHub(this.store, skillId);
|
|
@@ -1573,7 +2150,7 @@ export class ViewerServer {
|
|
|
1573
2150
|
method: "POST",
|
|
1574
2151
|
body: JSON.stringify({
|
|
1575
2152
|
visibility,
|
|
1576
|
-
groupId:
|
|
2153
|
+
groupId: null,
|
|
1577
2154
|
metadata: bundle.metadata,
|
|
1578
2155
|
bundle: bundle.bundle,
|
|
1579
2156
|
}),
|
|
@@ -1588,7 +2165,7 @@ export class ViewerServer {
|
|
|
1588
2165
|
name: skill.name,
|
|
1589
2166
|
description: skill.description,
|
|
1590
2167
|
version: skill.version,
|
|
1591
|
-
groupId:
|
|
2168
|
+
groupId: null,
|
|
1592
2169
|
visibility,
|
|
1593
2170
|
bundle: JSON.stringify(bundle.bundle),
|
|
1594
2171
|
qualityScore: skill.qualityScore,
|
|
@@ -1671,117 +2248,6 @@ export class ViewerServer {
|
|
|
1671
2248
|
return resolveHubClient(this.store, this.ctx);
|
|
1672
2249
|
}
|
|
1673
2250
|
|
|
1674
|
-
private extractGroupId(path: string): string {
|
|
1675
|
-
const m = path.match(/\/api\/sharing\/groups\/([^/]+)/);
|
|
1676
|
-
return m ? decodeURIComponent(m[1]) : "";
|
|
1677
|
-
}
|
|
1678
|
-
|
|
1679
|
-
private async serveSharingGroups(res: http.ServerResponse): Promise<void> {
|
|
1680
|
-
const hub = this.resolveHubConnection();
|
|
1681
|
-
if (!hub) return this.jsonResponse(res, { groups: [], error: "not_configured" });
|
|
1682
|
-
try {
|
|
1683
|
-
const data = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/groups", { method: "GET" }) as any;
|
|
1684
|
-
this.jsonResponse(res, { groups: Array.isArray(data?.groups) ? data.groups : [] });
|
|
1685
|
-
} catch (err) {
|
|
1686
|
-
this.jsonResponse(res, { groups: [], error: String(err) });
|
|
1687
|
-
}
|
|
1688
|
-
}
|
|
1689
|
-
|
|
1690
|
-
private handleSharingGroupCreate(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
1691
|
-
this.readBody(req, async (body) => {
|
|
1692
|
-
const hub = this.resolveHubConnection();
|
|
1693
|
-
if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1694
|
-
try {
|
|
1695
|
-
const parsed = JSON.parse(body || "{}");
|
|
1696
|
-
const data = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/groups", {
|
|
1697
|
-
method: "POST",
|
|
1698
|
-
body: JSON.stringify({ name: parsed.name, description: parsed.description }),
|
|
1699
|
-
}) as any;
|
|
1700
|
-
this.jsonResponse(res, { ok: true, ...data });
|
|
1701
|
-
} catch (err) {
|
|
1702
|
-
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1703
|
-
}
|
|
1704
|
-
});
|
|
1705
|
-
}
|
|
1706
|
-
|
|
1707
|
-
private handleSharingGroupUpdate(req: http.IncomingMessage, res: http.ServerResponse, p: string): void {
|
|
1708
|
-
this.readBody(req, async (body) => {
|
|
1709
|
-
const hub = this.resolveHubConnection();
|
|
1710
|
-
if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1711
|
-
const groupId = this.extractGroupId(p);
|
|
1712
|
-
try {
|
|
1713
|
-
const parsed = JSON.parse(body || "{}");
|
|
1714
|
-
await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}`, {
|
|
1715
|
-
method: "PUT",
|
|
1716
|
-
body: JSON.stringify({ name: parsed.name, description: parsed.description }),
|
|
1717
|
-
});
|
|
1718
|
-
this.jsonResponse(res, { ok: true });
|
|
1719
|
-
} catch (err) {
|
|
1720
|
-
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1721
|
-
}
|
|
1722
|
-
});
|
|
1723
|
-
}
|
|
1724
|
-
|
|
1725
|
-
private async handleSharingGroupDelete(res: http.ServerResponse, p: string): Promise<void> {
|
|
1726
|
-
const hub = this.resolveHubConnection();
|
|
1727
|
-
if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1728
|
-
const groupId = this.extractGroupId(p);
|
|
1729
|
-
try {
|
|
1730
|
-
await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}`, { method: "DELETE" });
|
|
1731
|
-
this.jsonResponse(res, { ok: true });
|
|
1732
|
-
} catch (err) {
|
|
1733
|
-
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1734
|
-
}
|
|
1735
|
-
}
|
|
1736
|
-
|
|
1737
|
-
private async serveSharingGroupMembers(res: http.ServerResponse, p: string): Promise<void> {
|
|
1738
|
-
const hub = this.resolveHubConnection();
|
|
1739
|
-
if (!hub) return this.jsonResponse(res, { members: [], error: "not_configured" });
|
|
1740
|
-
const groupId = this.extractGroupId(p);
|
|
1741
|
-
try {
|
|
1742
|
-
const data = await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}`, { method: "GET" }) as any;
|
|
1743
|
-
this.jsonResponse(res, { members: Array.isArray(data?.members) ? data.members : [] });
|
|
1744
|
-
} catch (err) {
|
|
1745
|
-
this.jsonResponse(res, { members: [], error: String(err) });
|
|
1746
|
-
}
|
|
1747
|
-
}
|
|
1748
|
-
|
|
1749
|
-
private handleSharingGroupAddMember(req: http.IncomingMessage, res: http.ServerResponse, p: string): void {
|
|
1750
|
-
this.readBody(req, async (body) => {
|
|
1751
|
-
const hub = this.resolveHubConnection();
|
|
1752
|
-
if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1753
|
-
const groupId = this.extractGroupId(p);
|
|
1754
|
-
try {
|
|
1755
|
-
const parsed = JSON.parse(body || "{}");
|
|
1756
|
-
await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}/members`, {
|
|
1757
|
-
method: "POST",
|
|
1758
|
-
body: JSON.stringify({ userId: parsed.userId }),
|
|
1759
|
-
});
|
|
1760
|
-
this.jsonResponse(res, { ok: true });
|
|
1761
|
-
} catch (err) {
|
|
1762
|
-
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1763
|
-
}
|
|
1764
|
-
});
|
|
1765
|
-
}
|
|
1766
|
-
|
|
1767
|
-
private handleSharingGroupRemoveMember(req: http.IncomingMessage, res: http.ServerResponse, p: string): void {
|
|
1768
|
-
this.readBody(req, async (body) => {
|
|
1769
|
-
const hub = this.resolveHubConnection();
|
|
1770
|
-
if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1771
|
-
const groupId = this.extractGroupId(p);
|
|
1772
|
-
try {
|
|
1773
|
-
const parsed = JSON.parse(body || "{}");
|
|
1774
|
-
await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}/members`, {
|
|
1775
|
-
method: "DELETE",
|
|
1776
|
-
body: JSON.stringify({ userId: parsed.userId }),
|
|
1777
|
-
});
|
|
1778
|
-
this.jsonResponse(res, { ok: true });
|
|
1779
|
-
} catch (err) {
|
|
1780
|
-
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1781
|
-
}
|
|
1782
|
-
});
|
|
1783
|
-
}
|
|
1784
|
-
|
|
1785
2251
|
private async serveSharingUsers(res: http.ServerResponse): Promise<void> {
|
|
1786
2252
|
const hub = this.resolveHubConnection();
|
|
1787
2253
|
if (!hub) return this.jsonResponse(res, { users: [], error: "not_configured" });
|
|
@@ -1800,7 +2266,14 @@ export class ViewerServer {
|
|
|
1800
2266
|
if (!hub) return this.jsonResponse(res, { tasks: [], error: "not_configured" });
|
|
1801
2267
|
try {
|
|
1802
2268
|
const data = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/shared-tasks", { method: "GET" }) as any;
|
|
1803
|
-
|
|
2269
|
+
const tasks = Array.isArray(data?.tasks) ? data.tasks : [];
|
|
2270
|
+
for (const tk of tasks) {
|
|
2271
|
+
if (!tk.summary && tk.sourceTaskId) {
|
|
2272
|
+
const local = this.store.getTask(tk.sourceTaskId);
|
|
2273
|
+
if (local) { tk.summary = local.summary; tk.title = tk.title || local.title; }
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
this.jsonResponse(res, { tasks });
|
|
1804
2277
|
} catch (err) {
|
|
1805
2278
|
this.jsonResponse(res, { tasks: [], error: String(err) });
|
|
1806
2279
|
}
|
|
@@ -1818,12 +2291,47 @@ export class ViewerServer {
|
|
|
1818
2291
|
}
|
|
1819
2292
|
}
|
|
1820
2293
|
|
|
2294
|
+
private async serveHubTaskDetail(res: http.ServerResponse, p: string): Promise<void> {
|
|
2295
|
+
const hub = this.resolveHubConnection();
|
|
2296
|
+
if (!hub) return this.jsonResponse(res, { error: "not_configured" }, 500);
|
|
2297
|
+
const m = p.match(/^\/api\/admin\/shared-tasks\/([^/]+)\/detail$/);
|
|
2298
|
+
if (!m) return this.jsonResponse(res, { error: "bad_request" }, 400);
|
|
2299
|
+
const taskId = decodeURIComponent(m[1]);
|
|
2300
|
+
try {
|
|
2301
|
+
const data = await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/shared-tasks/${encodeURIComponent(taskId)}/detail`, { method: "GET" }) as any;
|
|
2302
|
+
this.jsonResponse(res, data);
|
|
2303
|
+
} catch (err) {
|
|
2304
|
+
this.jsonResponse(res, { error: String(err) }, 500);
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
private async serveHubSkillDetail(res: http.ServerResponse, p: string): Promise<void> {
|
|
2309
|
+
const hub = this.resolveHubConnection();
|
|
2310
|
+
if (!hub) return this.jsonResponse(res, { error: "not_configured" }, 500);
|
|
2311
|
+
const m = p.match(/^\/api\/admin\/shared-skills\/([^/]+)\/detail$/);
|
|
2312
|
+
if (!m) return this.jsonResponse(res, { error: "bad_request" }, 400);
|
|
2313
|
+
const skillId = decodeURIComponent(m[1]);
|
|
2314
|
+
try {
|
|
2315
|
+
const data = await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/shared-skills/${encodeURIComponent(skillId)}/detail`, { method: "GET" }) as any;
|
|
2316
|
+
this.jsonResponse(res, data);
|
|
2317
|
+
} catch (err) {
|
|
2318
|
+
this.jsonResponse(res, { error: String(err) }, 500);
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
|
|
1821
2322
|
private async serveAdminSharedSkills(res: http.ServerResponse): Promise<void> {
|
|
1822
2323
|
const hub = this.resolveHubConnection();
|
|
1823
2324
|
if (!hub) return this.jsonResponse(res, { skills: [], error: "not_configured" });
|
|
1824
2325
|
try {
|
|
1825
2326
|
const data = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/shared-skills", { method: "GET" }) as any;
|
|
1826
|
-
|
|
2327
|
+
const skills = Array.isArray(data?.skills) ? data.skills : [];
|
|
2328
|
+
for (const sk of skills) {
|
|
2329
|
+
if (!sk.description && sk.sourceSkillId) {
|
|
2330
|
+
const local = this.store.getSkill(sk.sourceSkillId);
|
|
2331
|
+
if (local) { sk.description = sk.description || local.description; sk.name = sk.name || local.name; }
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
this.jsonResponse(res, { skills });
|
|
1827
2335
|
} catch (err) {
|
|
1828
2336
|
this.jsonResponse(res, { skills: [], error: String(err) });
|
|
1829
2337
|
}
|
|
@@ -1846,7 +2354,14 @@ export class ViewerServer {
|
|
|
1846
2354
|
if (!hub) return this.jsonResponse(res, { memories: [], error: "not_configured" });
|
|
1847
2355
|
try {
|
|
1848
2356
|
const data = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/shared-memories", { method: "GET" }) as any;
|
|
1849
|
-
|
|
2357
|
+
const memories = Array.isArray(data?.memories) ? data.memories : [];
|
|
2358
|
+
for (const m of memories) {
|
|
2359
|
+
if (!m.content && m.sourceChunkId) {
|
|
2360
|
+
const local = this.store.getChunk(m.sourceChunkId);
|
|
2361
|
+
if (local) { m.content = local.content; if (!m.summary && local.summary) m.summary = local.summary; }
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
this.jsonResponse(res, { memories });
|
|
1850
2365
|
} catch (err) {
|
|
1851
2366
|
this.jsonResponse(res, { memories: [], error: String(err) });
|
|
1852
2367
|
}
|
|
@@ -1864,7 +2379,121 @@ export class ViewerServer {
|
|
|
1864
2379
|
}
|
|
1865
2380
|
}
|
|
1866
2381
|
|
|
1867
|
-
private
|
|
2382
|
+
private async serveSharingNotifications(res: http.ServerResponse, url: URL): Promise<void> {
|
|
2383
|
+
const hub = this.resolveHubConnection();
|
|
2384
|
+
if (!hub) return this.jsonResponse(res, { notifications: [], unreadCount: 0 });
|
|
2385
|
+
try {
|
|
2386
|
+
const unread = url.searchParams.get("unread") === "1" ? "?unread=1" : "";
|
|
2387
|
+
const data = await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/notifications${unread}`) as any;
|
|
2388
|
+
this.jsonResponse(res, data);
|
|
2389
|
+
} catch {
|
|
2390
|
+
this.jsonResponse(res, { notifications: [], unreadCount: 0 });
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
private handleSharingNotificationsRead(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
2395
|
+
const hub = this.resolveHubConnection();
|
|
2396
|
+
if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
2397
|
+
this.readBody(req, async (raw) => {
|
|
2398
|
+
try {
|
|
2399
|
+
const body = JSON.parse(raw || "{}");
|
|
2400
|
+
await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications/read", { method: "POST", body: JSON.stringify(body) });
|
|
2401
|
+
this.jsonResponse(res, { ok: true });
|
|
2402
|
+
try {
|
|
2403
|
+
const data = (await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications?unread=1")) as any;
|
|
2404
|
+
const count = data?.unreadCount ?? 0;
|
|
2405
|
+
this.lastKnownNotifCount = count;
|
|
2406
|
+
this.broadcastNotifSSE({ type: "update", unreadCount: count });
|
|
2407
|
+
} catch { /* best effort */ }
|
|
2408
|
+
} catch (err) {
|
|
2409
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
2410
|
+
}
|
|
2411
|
+
});
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
private handleSharingNotificationsClear(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
2415
|
+
const hub = this.resolveHubConnection();
|
|
2416
|
+
if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
2417
|
+
this.readBody(req, async () => {
|
|
2418
|
+
try {
|
|
2419
|
+
await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications/clear", { method: "POST", body: "{}" });
|
|
2420
|
+
this.jsonResponse(res, { ok: true });
|
|
2421
|
+
this.broadcastNotifSSE({ type: "cleared", unreadCount: 0 });
|
|
2422
|
+
} catch (err) {
|
|
2423
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
2424
|
+
}
|
|
2425
|
+
});
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
private handleNotifSSE(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
2429
|
+
res.writeHead(200, {
|
|
2430
|
+
"Content-Type": "text/event-stream",
|
|
2431
|
+
"Cache-Control": "no-cache",
|
|
2432
|
+
Connection: "keep-alive",
|
|
2433
|
+
"Access-Control-Allow-Origin": "*",
|
|
2434
|
+
});
|
|
2435
|
+
res.write("data: {\"type\":\"connected\"}\n\n");
|
|
2436
|
+
this.notifSSEClients.push(res);
|
|
2437
|
+
if (!this.notifPollTimer) this.startNotifPoll();
|
|
2438
|
+
req.on("close", () => {
|
|
2439
|
+
this.notifSSEClients = this.notifSSEClients.filter((c) => c !== res);
|
|
2440
|
+
if (this.notifSSEClients.length === 0) this.stopNotifPoll();
|
|
2441
|
+
});
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
private broadcastNotifSSE(data: Record<string, unknown>): void {
|
|
2445
|
+
const msg = `data: ${JSON.stringify(data)}\n\n`;
|
|
2446
|
+
this.notifSSEClients = this.notifSSEClients.filter((c) => {
|
|
2447
|
+
try { c.write(msg); return true; } catch { return false; }
|
|
2448
|
+
});
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
private startNotifPoll(): void {
|
|
2452
|
+
this.stopNotifPoll();
|
|
2453
|
+
const tick = async () => {
|
|
2454
|
+
const hub = this.resolveHubConnection();
|
|
2455
|
+
if (!hub) return;
|
|
2456
|
+
try {
|
|
2457
|
+
const data = (await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications?unread=1")) as any;
|
|
2458
|
+
const count = data?.unreadCount ?? 0;
|
|
2459
|
+
if (count !== this.lastKnownNotifCount) {
|
|
2460
|
+
this.lastKnownNotifCount = count;
|
|
2461
|
+
this.broadcastNotifSSE({ type: "update", unreadCount: count });
|
|
2462
|
+
}
|
|
2463
|
+
} catch { /* ignore */ }
|
|
2464
|
+
};
|
|
2465
|
+
tick();
|
|
2466
|
+
this.notifPollTimer = setInterval(tick, 3000);
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
private stopNotifPoll(): void {
|
|
2470
|
+
if (this.notifPollTimer) { clearInterval(this.notifPollTimer); this.notifPollTimer = undefined; }
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
private startHubHeartbeat(): void {
|
|
2474
|
+
this.stopHubHeartbeat();
|
|
2475
|
+
const sendHeartbeat = async () => {
|
|
2476
|
+
try {
|
|
2477
|
+
const hub = this.resolveHubConnection();
|
|
2478
|
+
if (!hub) {
|
|
2479
|
+
const persisted = this.store.getClientHubConnection();
|
|
2480
|
+
if (persisted?.hubUrl && persisted?.userToken) {
|
|
2481
|
+
await hubRequestJson(persisted.hubUrl, persisted.userToken, "/api/v1/hub/heartbeat", { method: "POST" });
|
|
2482
|
+
}
|
|
2483
|
+
return;
|
|
2484
|
+
}
|
|
2485
|
+
await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/heartbeat", { method: "POST" });
|
|
2486
|
+
} catch { /* best-effort */ }
|
|
2487
|
+
};
|
|
2488
|
+
sendHeartbeat();
|
|
2489
|
+
this.hubHeartbeatTimer = setInterval(sendHeartbeat, ViewerServer.HUB_HEARTBEAT_INTERVAL_MS);
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
private stopHubHeartbeat(): void {
|
|
2493
|
+
if (this.hubHeartbeatTimer) { clearInterval(this.hubHeartbeatTimer); this.hubHeartbeatTimer = undefined; }
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
private getLocalIPs(): string[] {
|
|
1868
2497
|
const nets = os.networkInterfaces();
|
|
1869
2498
|
const ips: string[] = [];
|
|
1870
2499
|
for (const name of Object.keys(nets)) {
|
|
@@ -1874,6 +2503,11 @@ export class ViewerServer {
|
|
|
1874
2503
|
}
|
|
1875
2504
|
}
|
|
1876
2505
|
}
|
|
2506
|
+
return ips;
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
private serveLocalIPs(res: http.ServerResponse): void {
|
|
2510
|
+
const ips = this.getLocalIPs();
|
|
1877
2511
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1878
2512
|
res.end(JSON.stringify({ ips }));
|
|
1879
2513
|
}
|
|
@@ -1911,7 +2545,7 @@ export class ViewerServer {
|
|
|
1911
2545
|
}
|
|
1912
2546
|
|
|
1913
2547
|
private handleSaveConfig(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
1914
|
-
this.readBody(req, (body) => {
|
|
2548
|
+
this.readBody(req, async (body) => {
|
|
1915
2549
|
try {
|
|
1916
2550
|
const newCfg = JSON.parse(body);
|
|
1917
2551
|
const cfgPath = this.getOpenClawConfigPath();
|
|
@@ -1934,6 +2568,11 @@ export class ViewerServer {
|
|
|
1934
2568
|
if (!entry.config) entry.config = {};
|
|
1935
2569
|
const config = entry.config as Record<string, unknown>;
|
|
1936
2570
|
|
|
2571
|
+
const oldSharing = config.sharing as Record<string, unknown> | undefined;
|
|
2572
|
+
const oldSharingRole = oldSharing?.role as string | undefined;
|
|
2573
|
+
const oldSharingEnabled = Boolean(oldSharing?.enabled);
|
|
2574
|
+
const oldClientHubAddress = String((oldSharing?.client as Record<string, unknown>)?.hubAddress || "");
|
|
2575
|
+
|
|
1937
2576
|
if (newCfg.embedding) config.embedding = newCfg.embedding;
|
|
1938
2577
|
if (newCfg.summarizer) config.summarizer = newCfg.summarizer;
|
|
1939
2578
|
if (newCfg.skillEvolution) config.skillEvolution = newCfg.skillEvolution;
|
|
@@ -1942,16 +2581,76 @@ export class ViewerServer {
|
|
|
1942
2581
|
if (newCfg.sharing !== undefined) {
|
|
1943
2582
|
const existing = (config.sharing as Record<string, unknown>) || {};
|
|
1944
2583
|
const merged = { ...existing, ...newCfg.sharing };
|
|
1945
|
-
// Deep-merge capabilities so new keys don't wipe existing ones
|
|
1946
2584
|
if (newCfg.sharing.capabilities && existing.capabilities) {
|
|
1947
2585
|
merged.capabilities = { ...(existing.capabilities as Record<string, unknown>), ...newCfg.sharing.capabilities };
|
|
1948
2586
|
}
|
|
2587
|
+
if (merged.role === "client" && merged.client) {
|
|
2588
|
+
const clientCfg = merged.client as Record<string, unknown>;
|
|
2589
|
+
const addr = String(clientCfg.hubAddress || "");
|
|
2590
|
+
if (addr) {
|
|
2591
|
+
const localIPs = this.getLocalIPs();
|
|
2592
|
+
localIPs.push("127.0.0.1", "localhost", "0.0.0.0");
|
|
2593
|
+
try {
|
|
2594
|
+
const u = new URL(addr.startsWith("http") ? addr : `http://${addr}`);
|
|
2595
|
+
if (localIPs.includes(u.hostname)) {
|
|
2596
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2597
|
+
res.end(JSON.stringify({ error: "cannot_join_self" }));
|
|
2598
|
+
return;
|
|
2599
|
+
}
|
|
2600
|
+
} catch {}
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
const newRole = merged.role as string | undefined;
|
|
2605
|
+
const newEnabled = Boolean(merged.enabled);
|
|
2606
|
+
|
|
2607
|
+
// Detect disabling sharing or switching away from hub mode
|
|
2608
|
+
const wasHub = oldSharingEnabled && oldSharingRole === "hub";
|
|
2609
|
+
const isHub = newEnabled && newRole === "hub";
|
|
2610
|
+
if (wasHub && !isHub) {
|
|
2611
|
+
await this.notifyHubShutdown();
|
|
2612
|
+
this.stopHubHeartbeat();
|
|
2613
|
+
this.log.info("Hub shutting down: notified connected clients");
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
// Detect disabling sharing or switching away from client mode
|
|
2617
|
+
const wasClient = oldSharingEnabled && oldSharingRole === "client";
|
|
2618
|
+
const isClient = newEnabled && newRole === "client";
|
|
2619
|
+
if (wasClient && !isClient) {
|
|
2620
|
+
this.notifyHubLeave();
|
|
2621
|
+
this.store.clearClientHubConnection();
|
|
2622
|
+
this.log.info("Cleared client hub connection (sharing disabled or role changed)");
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
// Detect switching to a different Hub while still in client mode
|
|
2626
|
+
if (wasClient && isClient) {
|
|
2627
|
+
const newClientAddr = String((merged.client as Record<string, unknown>)?.hubAddress || "");
|
|
2628
|
+
if (newClientAddr && oldClientHubAddress && normalizeHubUrl(newClientAddr) !== normalizeHubUrl(oldClientHubAddress)) {
|
|
2629
|
+
this.notifyHubLeave();
|
|
2630
|
+
this.store.clearClientHubConnection();
|
|
2631
|
+
this.log.info("Cleared client hub connection (switched to different Hub)");
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
if (merged.role === "hub") {
|
|
2636
|
+
merged.client = { hubAddress: "", userToken: "", teamToken: "" };
|
|
2637
|
+
} else if (merged.role === "client") {
|
|
2638
|
+
merged.hub = { port: 18800, teamName: "", teamToken: "" };
|
|
2639
|
+
}
|
|
1949
2640
|
config.sharing = merged;
|
|
1950
2641
|
}
|
|
1951
2642
|
|
|
1952
2643
|
fs.mkdirSync(path.dirname(cfgPath), { recursive: true });
|
|
1953
2644
|
fs.writeFileSync(cfgPath, JSON.stringify(raw, null, 2), "utf-8");
|
|
1954
2645
|
this.log.info("Plugin config updated via Viewer");
|
|
2646
|
+
this.stopHubHeartbeat();
|
|
2647
|
+
|
|
2648
|
+
// When switching to client mode, immediately send join request
|
|
2649
|
+
const finalSharing = config.sharing as Record<string, unknown> | undefined;
|
|
2650
|
+
if (finalSharing?.role === "client" && oldSharingRole !== "client") {
|
|
2651
|
+
this.autoJoinOnSave(finalSharing).catch(e => this.log.warn(`Auto-join on save failed: ${e}`));
|
|
2652
|
+
}
|
|
2653
|
+
|
|
1955
2654
|
this.jsonResponse(res, { ok: true });
|
|
1956
2655
|
} catch (e) {
|
|
1957
2656
|
this.log.warn(`handleSaveConfig error: ${e}`);
|
|
@@ -1961,6 +2660,87 @@ export class ViewerServer {
|
|
|
1961
2660
|
});
|
|
1962
2661
|
}
|
|
1963
2662
|
|
|
2663
|
+
private async autoJoinOnSave(sharing: Record<string, unknown>): Promise<void> {
|
|
2664
|
+
const clientCfg = sharing.client as Record<string, unknown> | undefined;
|
|
2665
|
+
const hubAddress = String(clientCfg?.hubAddress || "");
|
|
2666
|
+
const teamToken = String(clientCfg?.teamToken || "");
|
|
2667
|
+
if (!hubAddress || !teamToken) return;
|
|
2668
|
+
const hubUrl = normalizeHubUrl(hubAddress);
|
|
2669
|
+
const os = await import("os");
|
|
2670
|
+
const nickname = String(clientCfg?.nickname || "");
|
|
2671
|
+
const username = nickname || os.userInfo().username || "user";
|
|
2672
|
+
const hostname = os.hostname() || "unknown";
|
|
2673
|
+
const result = await hubRequestJson(hubUrl, "", "/api/v1/hub/join", {
|
|
2674
|
+
method: "POST",
|
|
2675
|
+
body: JSON.stringify({ teamToken, username, deviceName: hostname }),
|
|
2676
|
+
}) as any;
|
|
2677
|
+
this.store.setClientHubConnection({
|
|
2678
|
+
hubUrl,
|
|
2679
|
+
userId: String(result.userId || ""),
|
|
2680
|
+
username,
|
|
2681
|
+
userToken: result.userToken || "",
|
|
2682
|
+
role: "member",
|
|
2683
|
+
connectedAt: Date.now(),
|
|
2684
|
+
});
|
|
2685
|
+
this.log.info(`Auto-join on save: status=${result.status}, userId=${result.userId}`);
|
|
2686
|
+
if (result.userToken) {
|
|
2687
|
+
this.startHubHeartbeat();
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
private async notifyHubLeave(): Promise<void> {
|
|
2692
|
+
try {
|
|
2693
|
+
const hub = this.resolveHubConnection();
|
|
2694
|
+
if (hub) {
|
|
2695
|
+
await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/leave", { method: "POST" });
|
|
2696
|
+
this.log.info("Notified Hub of voluntary leave");
|
|
2697
|
+
return;
|
|
2698
|
+
}
|
|
2699
|
+
const persisted = this.store.getClientHubConnection();
|
|
2700
|
+
if (persisted?.hubUrl && persisted?.userToken) {
|
|
2701
|
+
await hubRequestJson(persisted.hubUrl, persisted.userToken, "/api/v1/hub/leave", { method: "POST" });
|
|
2702
|
+
this.log.info("Notified Hub of voluntary leave (persisted connection)");
|
|
2703
|
+
}
|
|
2704
|
+
} catch (e) {
|
|
2705
|
+
this.log.warn(`Failed to notify Hub of leave: ${e}`);
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2709
|
+
private async notifyHubShutdown(): Promise<void> {
|
|
2710
|
+
try {
|
|
2711
|
+
const sharing = this.ctx?.config.sharing;
|
|
2712
|
+
if (!sharing || sharing.role !== "hub") return;
|
|
2713
|
+
const hubPort = sharing.hub?.port ?? 18800;
|
|
2714
|
+
const authPath = path.join(this.dataDir, "hub-auth.json");
|
|
2715
|
+
let adminToken: string | undefined;
|
|
2716
|
+
try {
|
|
2717
|
+
const authData = JSON.parse(fs.readFileSync(authPath, "utf8"));
|
|
2718
|
+
adminToken = authData?.bootstrapAdminToken;
|
|
2719
|
+
} catch { return; }
|
|
2720
|
+
if (!adminToken) return;
|
|
2721
|
+
|
|
2722
|
+
const users = this.store.listHubUsers("active");
|
|
2723
|
+
const { v4: uuidv4 } = require("uuid");
|
|
2724
|
+
for (const u of users) {
|
|
2725
|
+
try {
|
|
2726
|
+
this.store.insertHubNotification({
|
|
2727
|
+
id: uuidv4(),
|
|
2728
|
+
userId: u.id,
|
|
2729
|
+
type: "hub_shutdown",
|
|
2730
|
+
resource: "hub",
|
|
2731
|
+
title: "Hub is shutting down",
|
|
2732
|
+
message: "The Hub server is shutting down. You may be disconnected.",
|
|
2733
|
+
});
|
|
2734
|
+
} catch (e) {
|
|
2735
|
+
this.log.warn(`Failed to insert shutdown notification for user ${u.id}: ${e}`);
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
this.log.info(`Hub shutdown: notified ${users.length} approved user(s)`);
|
|
2739
|
+
} catch (e) {
|
|
2740
|
+
this.log.warn(`notifyHubShutdown error: ${e}`);
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
|
|
1964
2744
|
private handleUpdateUsername(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
1965
2745
|
this.readBody(req, async (body) => {
|
|
1966
2746
|
if (!this.ctx) return this.jsonResponse(res, { error: "sharing_unavailable" });
|
|
@@ -2014,6 +2794,15 @@ export class ViewerServer {
|
|
|
2014
2794
|
try {
|
|
2015
2795
|
const { hubUrl } = JSON.parse(body);
|
|
2016
2796
|
if (!hubUrl) { this.jsonResponse(res, { ok: false, error: "hubUrl is required" }); return; }
|
|
2797
|
+
try {
|
|
2798
|
+
const localIPs = this.getLocalIPs();
|
|
2799
|
+
localIPs.push("127.0.0.1", "localhost", "0.0.0.0");
|
|
2800
|
+
const parsed = new URL(hubUrl.startsWith("http") ? hubUrl : `http://${hubUrl}`);
|
|
2801
|
+
if (localIPs.includes(parsed.hostname)) {
|
|
2802
|
+
this.jsonResponse(res, { ok: false, error: "cannot_join_self" });
|
|
2803
|
+
return;
|
|
2804
|
+
}
|
|
2805
|
+
} catch {}
|
|
2017
2806
|
const url = hubUrl.replace(/\/+$/, "") + "/api/v1/hub/info";
|
|
2018
2807
|
const ctrl = new AbortController();
|
|
2019
2808
|
const timeout = setTimeout(() => ctrl.abort(), 8000);
|
|
@@ -2405,7 +3194,7 @@ export class ViewerServer {
|
|
|
2405
3194
|
|
|
2406
3195
|
private getOpenClawHome(): string {
|
|
2407
3196
|
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
2408
|
-
return path.join(home, ".openclaw");
|
|
3197
|
+
return process.env.OPENCLAW_STATE_DIR || path.join(home, ".openclaw");
|
|
2409
3198
|
}
|
|
2410
3199
|
|
|
2411
3200
|
private handleCleanupPolluted(res: http.ServerResponse): void {
|
|
@@ -2434,7 +3223,7 @@ export class ViewerServer {
|
|
|
2434
3223
|
try {
|
|
2435
3224
|
const ocHome = this.getOpenClawHome();
|
|
2436
3225
|
const memoryDir = path.join(ocHome, "memory");
|
|
2437
|
-
const
|
|
3226
|
+
const agentsDir = path.join(ocHome, "agents");
|
|
2438
3227
|
|
|
2439
3228
|
const sqliteFiles: Array<{ file: string; chunks: number }> = [];
|
|
2440
3229
|
if (fs.existsSync(memoryDir)) {
|
|
@@ -2453,31 +3242,36 @@ export class ViewerServer {
|
|
|
2453
3242
|
|
|
2454
3243
|
let sessionCount = 0;
|
|
2455
3244
|
let messageCount = 0;
|
|
2456
|
-
if (fs.existsSync(
|
|
2457
|
-
const
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
3245
|
+
if (fs.existsSync(agentsDir)) {
|
|
3246
|
+
for (const entry of fs.readdirSync(agentsDir, { withFileTypes: true })) {
|
|
3247
|
+
if (!entry.isDirectory()) continue;
|
|
3248
|
+
const sessDir = path.join(agentsDir, entry.name, "sessions");
|
|
3249
|
+
if (!fs.existsSync(sessDir)) continue;
|
|
3250
|
+
const jsonlFiles = fs.readdirSync(sessDir).filter(f => f.includes(".jsonl"));
|
|
3251
|
+
sessionCount += jsonlFiles.length;
|
|
3252
|
+
for (const f of jsonlFiles) {
|
|
3253
|
+
try {
|
|
3254
|
+
const content = fs.readFileSync(path.join(sessDir, f), "utf-8");
|
|
3255
|
+
const lines = content.split("\n").filter(l => l.trim());
|
|
3256
|
+
for (const line of lines) {
|
|
3257
|
+
try {
|
|
3258
|
+
const obj = JSON.parse(line);
|
|
3259
|
+
if (obj.type === "message") {
|
|
3260
|
+
const role = obj.message?.role ?? obj.role;
|
|
3261
|
+
if (role === "user" || role === "assistant") {
|
|
3262
|
+
const mc = obj.message?.content ?? obj.content;
|
|
3263
|
+
let txt = "";
|
|
3264
|
+
if (typeof mc === "string") txt = mc;
|
|
3265
|
+
else if (Array.isArray(mc)) txt = mc.filter((p: any) => p.type === "text" && p.text).map((p: any) => p.text).join("\n");
|
|
3266
|
+
else txt = JSON.stringify(mc);
|
|
3267
|
+
if (role === "user") txt = stripInboundMetadata(txt);
|
|
3268
|
+
if (txt && txt.length >= 10) messageCount++;
|
|
3269
|
+
}
|
|
2476
3270
|
}
|
|
2477
|
-
}
|
|
2478
|
-
}
|
|
2479
|
-
}
|
|
2480
|
-
}
|
|
3271
|
+
} catch { /* skip bad lines */ }
|
|
3272
|
+
}
|
|
3273
|
+
} catch { /* skip unreadable */ }
|
|
3274
|
+
}
|
|
2481
3275
|
}
|
|
2482
3276
|
}
|
|
2483
3277
|
|