@memtensor/memos-local-openclaw-plugin 1.0.4-beta.6 → 1.0.4-beta.8
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/README.md +38 -21
- package/dist/capture/index.d.ts +1 -1
- package/dist/capture/index.d.ts.map +1 -1
- package/dist/capture/index.js +29 -3
- package/dist/capture/index.js.map +1 -1
- package/dist/client/connector.d.ts +29 -0
- package/dist/client/connector.d.ts.map +1 -0
- package/dist/client/connector.js +231 -0
- package/dist/client/connector.js.map +1 -0
- package/dist/client/hub.d.ts +61 -0
- package/dist/client/hub.d.ts.map +1 -0
- package/dist/client/hub.js +170 -0
- package/dist/client/hub.js.map +1 -0
- package/dist/client/skill-sync.d.ts +36 -0
- package/dist/client/skill-sync.d.ts.map +1 -0
- package/dist/client/skill-sync.js +226 -0
- package/dist/client/skill-sync.js.map +1 -0
- package/dist/config.d.ts +2 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +70 -3
- package/dist/config.js.map +1 -1
- package/dist/embedding/index.d.ts +4 -2
- package/dist/embedding/index.d.ts.map +1 -1
- package/dist/embedding/index.js +17 -1
- package/dist/embedding/index.js.map +1 -1
- package/dist/hub/auth.d.ts +19 -0
- package/dist/hub/auth.d.ts.map +1 -0
- package/dist/hub/auth.js +70 -0
- package/dist/hub/auth.js.map +1 -0
- package/dist/hub/server.d.ts +48 -0
- package/dist/hub/server.d.ts.map +1 -0
- package/dist/hub/server.js +922 -0
- package/dist/hub/server.js.map +1 -0
- package/dist/hub/user-manager.d.ts +31 -0
- package/dist/hub/user-manager.d.ts.map +1 -0
- package/dist/hub/user-manager.js +129 -0
- package/dist/hub/user-manager.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -4
- package/dist/index.js.map +1 -1
- package/dist/ingest/providers/index.d.ts +10 -2
- package/dist/ingest/providers/index.d.ts.map +1 -1
- package/dist/ingest/providers/index.js +203 -6
- package/dist/ingest/providers/index.js.map +1 -1
- package/dist/ingest/providers/openai.d.ts +1 -0
- package/dist/ingest/providers/openai.d.ts.map +1 -1
- package/dist/ingest/providers/openai.js +1 -0
- package/dist/ingest/providers/openai.js.map +1 -1
- package/dist/ingest/task-processor.js +1 -1
- package/dist/ingest/task-processor.js.map +1 -1
- package/dist/openclaw-api.d.ts +53 -0
- package/dist/openclaw-api.d.ts.map +1 -0
- package/dist/openclaw-api.js +189 -0
- package/dist/openclaw-api.js.map +1 -0
- package/dist/recall/engine.js +1 -1
- package/dist/recall/engine.js.map +1 -1
- package/dist/shared/llm-call.d.ts +4 -1
- package/dist/shared/llm-call.d.ts.map +1 -1
- package/dist/shared/llm-call.js +14 -1
- package/dist/shared/llm-call.js.map +1 -1
- package/dist/sharing/types.contract.d.ts +2 -0
- package/dist/sharing/types.contract.d.ts.map +1 -0
- package/dist/sharing/types.contract.js +3 -0
- package/dist/sharing/types.contract.js.map +1 -0
- package/dist/sharing/types.d.ts +80 -0
- package/dist/sharing/types.d.ts.map +1 -0
- package/dist/sharing/types.js +3 -0
- package/dist/sharing/types.js.map +1 -0
- package/dist/skill/evaluator.d.ts.map +1 -1
- package/dist/skill/evaluator.js +2 -2
- package/dist/skill/evaluator.js.map +1 -1
- package/dist/skill/generator.d.ts.map +1 -1
- package/dist/skill/generator.js +4 -4
- package/dist/skill/generator.js.map +1 -1
- package/dist/skill/upgrader.js +1 -1
- package/dist/skill/upgrader.js.map +1 -1
- package/dist/skill/validator.js +1 -1
- package/dist/skill/validator.js.map +1 -1
- package/dist/storage/ensure-binding.d.ts.map +1 -1
- package/dist/storage/ensure-binding.js +3 -1
- package/dist/storage/ensure-binding.js.map +1 -1
- package/dist/storage/sqlite.d.ts +332 -1
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +913 -4
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +3 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/memory-search.d.ts +5 -2
- package/dist/tools/memory-search.d.ts.map +1 -1
- package/dist/tools/memory-search.js +50 -7
- package/dist/tools/memory-search.js.map +1 -1
- package/dist/tools/network-memory-detail.d.ts +4 -0
- package/dist/tools/network-memory-detail.d.ts.map +1 -0
- package/dist/tools/network-memory-detail.js +34 -0
- package/dist/tools/network-memory-detail.js.map +1 -0
- package/dist/types.d.ts +48 -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 +4299 -511
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +65 -0
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +1844 -90
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +767 -41
- package/openclaw.plugin.json +3 -2
- package/package.json +3 -3
- package/scripts/postinstall.cjs +282 -45
- package/skill/memos-memory-guide/SKILL.md +82 -20
- package/src/capture/index.ts +30 -2
- package/src/client/connector.ts +225 -0
- package/src/client/hub.ts +207 -0
- package/src/client/skill-sync.ts +216 -0
- package/src/config.ts +92 -3
- package/src/embedding/index.ts +21 -1
- package/src/hub/auth.ts +78 -0
- package/src/hub/server.ts +906 -0
- package/src/hub/user-manager.ts +143 -0
- package/src/index.ts +13 -5
- package/src/ingest/providers/index.ts +240 -6
- package/src/ingest/providers/openai.ts +1 -1
- package/src/ingest/task-processor.ts +1 -1
- package/src/openclaw-api.ts +287 -0
- package/src/recall/engine.ts +1 -1
- package/src/shared/llm-call.ts +18 -2
- package/src/sharing/types.contract.ts +40 -0
- package/src/sharing/types.ts +102 -0
- package/src/skill/evaluator.ts +3 -2
- package/src/skill/generator.ts +6 -4
- package/src/skill/upgrader.ts +1 -1
- package/src/skill/validator.ts +1 -1
- package/src/storage/ensure-binding.ts +3 -1
- package/src/storage/sqlite.ts +1164 -4
- package/src/tools/index.ts +1 -0
- package/src/tools/memory-search.ts +58 -8
- package/src/tools/network-memory-detail.ts +34 -0
- package/src/types.ts +43 -2
- package/src/viewer/html.ts +4299 -511
- package/src/viewer/server.ts +1688 -73
package/dist/viewer/server.js
CHANGED
|
@@ -51,6 +51,10 @@ const vector_1 = require("../storage/vector");
|
|
|
51
51
|
const task_processor_1 = require("../ingest/task-processor");
|
|
52
52
|
const engine_1 = require("../recall/engine");
|
|
53
53
|
const evolver_1 = require("../skill/evolver");
|
|
54
|
+
const config_1 = require("../config");
|
|
55
|
+
const connector_1 = require("../client/connector");
|
|
56
|
+
const hub_1 = require("../client/hub");
|
|
57
|
+
const skill_sync_1 = require("../client/skill-sync");
|
|
54
58
|
const html_1 = require("./html");
|
|
55
59
|
const uuid_1 = require("uuid");
|
|
56
60
|
function normalizeTimestamp(ts) {
|
|
@@ -87,6 +91,11 @@ class ViewerServer {
|
|
|
87
91
|
ppAbort = false;
|
|
88
92
|
ppState = { running: false, done: false, stopped: false, processed: 0, total: 0, tasksCreated: 0, skillsCreated: 0, errors: 0, skippedSessions: 0, totalSessions: 0 };
|
|
89
93
|
ppSSEClients = [];
|
|
94
|
+
notifSSEClients = [];
|
|
95
|
+
notifPollTimer;
|
|
96
|
+
lastKnownNotifCount = 0;
|
|
97
|
+
hubHeartbeatTimer;
|
|
98
|
+
static HUB_HEARTBEAT_INTERVAL_MS = 45_000;
|
|
90
99
|
constructor(opts) {
|
|
91
100
|
this.store = opts.store;
|
|
92
101
|
this.embedder = opts.embedder;
|
|
@@ -105,16 +114,17 @@ class ViewerServer {
|
|
|
105
114
|
this.server.on("error", (err) => {
|
|
106
115
|
if (err.code === "EADDRINUSE") {
|
|
107
116
|
this.log.warn(`Viewer port ${this.port} in use, trying ${this.port + 1}`);
|
|
108
|
-
this.server.listen(this.port + 1, "
|
|
117
|
+
this.server.listen(this.port + 1, "0.0.0.0");
|
|
109
118
|
}
|
|
110
119
|
else {
|
|
111
120
|
reject(err);
|
|
112
121
|
}
|
|
113
122
|
});
|
|
114
|
-
this.server.listen(this.port, "
|
|
123
|
+
this.server.listen(this.port, "0.0.0.0", () => {
|
|
115
124
|
const addr = this.server.address();
|
|
116
125
|
const actualPort = typeof addr === "object" && addr ? addr.port : this.port;
|
|
117
126
|
this.autoCleanupPolluted();
|
|
127
|
+
this.startHubHeartbeat();
|
|
118
128
|
resolve(`http://127.0.0.1:${actualPort}`);
|
|
119
129
|
});
|
|
120
130
|
});
|
|
@@ -137,6 +147,15 @@ class ViewerServer {
|
|
|
137
147
|
}
|
|
138
148
|
}
|
|
139
149
|
stop() {
|
|
150
|
+
this.stopHubHeartbeat();
|
|
151
|
+
this.stopNotifPoll();
|
|
152
|
+
for (const c of this.notifSSEClients) {
|
|
153
|
+
try {
|
|
154
|
+
c.end();
|
|
155
|
+
}
|
|
156
|
+
catch { }
|
|
157
|
+
}
|
|
158
|
+
this.notifSSEClients = [];
|
|
140
159
|
this.server?.close();
|
|
141
160
|
this.server = null;
|
|
142
161
|
}
|
|
@@ -224,8 +243,18 @@ class ViewerServer {
|
|
|
224
243
|
}
|
|
225
244
|
if (p === "/api/memories" && req.method === "GET")
|
|
226
245
|
this.serveMemories(res, url);
|
|
246
|
+
else if (p === "/api/memories/share-local" && req.method === "POST")
|
|
247
|
+
this.handleMemoryLocalShare(req, res);
|
|
248
|
+
else if (p === "/api/memories/unshare-local" && req.method === "POST")
|
|
249
|
+
this.handleMemoryLocalUnshare(req, res);
|
|
250
|
+
else if (p.match(/^\/api\/memory\/[^/]+\/scope$/) && req.method === "PUT")
|
|
251
|
+
this.handleMemoryScope(req, res, p);
|
|
252
|
+
else if (p.match(/^\/api\/task\/[^/]+\/scope$/) && req.method === "PUT")
|
|
253
|
+
this.handleTaskScope(req, res, p);
|
|
254
|
+
else if (p.match(/^\/api\/skill\/[^/]+\/scope$/) && req.method === "PUT")
|
|
255
|
+
this.handleSkillScope(req, res, p);
|
|
227
256
|
else if (p === "/api/stats")
|
|
228
|
-
this.serveStats(res);
|
|
257
|
+
this.serveStats(res, url);
|
|
229
258
|
else if (p === "/api/metrics")
|
|
230
259
|
this.serveMetrics(res, url);
|
|
231
260
|
else if (p === "/api/tool-metrics")
|
|
@@ -270,6 +299,80 @@ class ViewerServer {
|
|
|
270
299
|
this.serveLogs(res, url);
|
|
271
300
|
else if (p === "/api/log-tools" && req.method === "GET")
|
|
272
301
|
this.serveLogTools(res);
|
|
302
|
+
else if (p === "/api/sharing/status" && req.method === "GET")
|
|
303
|
+
this.serveSharingStatus(res);
|
|
304
|
+
else if (p === "/api/sharing/pending-users" && req.method === "GET")
|
|
305
|
+
this.serveSharingPendingUsers(res);
|
|
306
|
+
else if (p === "/api/sharing/approve-user" && req.method === "POST")
|
|
307
|
+
this.handleSharingApproveUser(req, res);
|
|
308
|
+
else if (p === "/api/sharing/reject-user" && req.method === "POST")
|
|
309
|
+
this.handleSharingRejectUser(req, res);
|
|
310
|
+
else if (p === "/api/sharing/remove-user" && req.method === "POST")
|
|
311
|
+
this.handleSharingRemoveUser(req, res);
|
|
312
|
+
else if (p === "/api/sharing/change-role" && req.method === "POST")
|
|
313
|
+
this.handleSharingChangeRole(req, res);
|
|
314
|
+
else if (p === "/api/sharing/retry-join" && req.method === "POST")
|
|
315
|
+
this.handleRetryJoin(req, res);
|
|
316
|
+
else if (p === "/api/sharing/search/memories" && req.method === "POST")
|
|
317
|
+
this.handleSharingMemorySearch(req, res);
|
|
318
|
+
else if (p === "/api/sharing/memories/list" && req.method === "GET")
|
|
319
|
+
this.serveSharingMemoryList(res, url);
|
|
320
|
+
else if (p === "/api/sharing/tasks/list" && req.method === "GET")
|
|
321
|
+
this.serveSharingTaskList(res, url);
|
|
322
|
+
else if (p === "/api/sharing/skills/list" && req.method === "GET")
|
|
323
|
+
this.serveSharingSkillList(res, url);
|
|
324
|
+
else if (p === "/api/sharing/memory-detail" && req.method === "POST")
|
|
325
|
+
this.handleSharingMemoryDetail(req, res);
|
|
326
|
+
else if (p === "/api/sharing/search/skills" && req.method === "GET")
|
|
327
|
+
this.serveSharingSkillSearch(res, url);
|
|
328
|
+
else if (p === "/api/sharing/tasks/share" && req.method === "POST")
|
|
329
|
+
this.handleSharingTaskShare(req, res);
|
|
330
|
+
else if (p === "/api/sharing/tasks/unshare" && req.method === "POST")
|
|
331
|
+
this.handleSharingTaskUnshare(req, res);
|
|
332
|
+
else if (p === "/api/sharing/update-username" && req.method === "POST")
|
|
333
|
+
this.handleUpdateUsername(req, res);
|
|
334
|
+
else if (p === "/api/sharing/rename-user" && req.method === "POST")
|
|
335
|
+
this.handleAdminRenameUser(req, res);
|
|
336
|
+
else if (p === "/api/sharing/test-hub" && req.method === "POST")
|
|
337
|
+
this.handleTestHubConnection(req, res);
|
|
338
|
+
else if (p === "/api/sharing/memories/share" && req.method === "POST")
|
|
339
|
+
this.handleSharingMemoryShare(req, res);
|
|
340
|
+
else if (p === "/api/sharing/memories/unshare" && req.method === "POST")
|
|
341
|
+
this.handleSharingMemoryUnshare(req, res);
|
|
342
|
+
else if (p === "/api/sharing/skills/pull" && req.method === "POST")
|
|
343
|
+
this.handleSharingSkillPull(req, res);
|
|
344
|
+
else if (p === "/api/sharing/skills/share" && req.method === "POST")
|
|
345
|
+
this.handleSharingSkillShare(req, res);
|
|
346
|
+
else if (p === "/api/sharing/skills/unshare" && req.method === "POST")
|
|
347
|
+
this.handleSharingSkillUnshare(req, res);
|
|
348
|
+
else if (p === "/api/sharing/users" && req.method === "GET")
|
|
349
|
+
this.serveSharingUsers(res);
|
|
350
|
+
else if (p === "/api/sharing/notifications" && req.method === "GET")
|
|
351
|
+
this.serveSharingNotifications(res, url);
|
|
352
|
+
else if (p === "/api/sharing/notifications/read" && req.method === "POST")
|
|
353
|
+
this.handleSharingNotificationsRead(req, res);
|
|
354
|
+
else if (p === "/api/sharing/notifications/clear" && req.method === "POST")
|
|
355
|
+
this.handleSharingNotificationsClear(req, res);
|
|
356
|
+
else if (p === "/api/notifications/stream" && req.method === "GET")
|
|
357
|
+
this.handleNotifSSE(req, res);
|
|
358
|
+
else if (p === "/api/admin/shared-tasks" && req.method === "GET")
|
|
359
|
+
this.serveAdminSharedTasks(res);
|
|
360
|
+
else if (p.match(/^\/api\/admin\/shared-tasks\/[^/]+\/detail$/) && req.method === "GET")
|
|
361
|
+
this.serveHubTaskDetail(res, p);
|
|
362
|
+
else if (p.match(/^\/api\/admin\/shared-tasks\/[^/]+$/) && req.method === "DELETE")
|
|
363
|
+
this.handleAdminDeleteTask(res, p);
|
|
364
|
+
else if (p === "/api/admin/shared-skills" && req.method === "GET")
|
|
365
|
+
this.serveAdminSharedSkills(res);
|
|
366
|
+
else if (p.match(/^\/api\/admin\/shared-skills\/[^/]+\/detail$/) && req.method === "GET")
|
|
367
|
+
this.serveHubSkillDetail(res, p);
|
|
368
|
+
else if (p.match(/^\/api\/admin\/shared-skills\/[^/]+$/) && req.method === "DELETE")
|
|
369
|
+
this.handleAdminDeleteSkill(res, p);
|
|
370
|
+
else if (p === "/api/admin/shared-memories" && req.method === "GET")
|
|
371
|
+
this.serveAdminSharedMemories(res);
|
|
372
|
+
else if (p.match(/^\/api\/admin\/shared-memories\/[^/]+$/) && req.method === "DELETE")
|
|
373
|
+
this.handleAdminDeleteMemory(res, p);
|
|
374
|
+
else if (p === "/api/local-ips" && req.method === "GET")
|
|
375
|
+
this.serveLocalIPs(res);
|
|
273
376
|
else if (p === "/api/config" && req.method === "GET")
|
|
274
377
|
this.serveConfig(res);
|
|
275
378
|
else if (p === "/api/config" && req.method === "PUT")
|
|
@@ -439,7 +542,12 @@ class ViewerServer {
|
|
|
439
542
|
conditions.push("role = ?");
|
|
440
543
|
params.push(role);
|
|
441
544
|
}
|
|
442
|
-
if (owner) {
|
|
545
|
+
if (owner && owner.startsWith("agent:")) {
|
|
546
|
+
const agentPrefix = owner + ":";
|
|
547
|
+
conditions.push("(owner = ? OR (owner = 'public' AND session_key LIKE ?))");
|
|
548
|
+
params.push(owner, agentPrefix + "%");
|
|
549
|
+
}
|
|
550
|
+
else if (owner) {
|
|
443
551
|
conditions.push("owner = ?");
|
|
444
552
|
params.push(owner);
|
|
445
553
|
}
|
|
@@ -455,15 +563,36 @@ class ViewerServer {
|
|
|
455
563
|
const totalRow = db.prepare("SELECT COUNT(*) as count FROM chunks" + where).get(...params);
|
|
456
564
|
const rawMemories = db.prepare("SELECT * FROM chunks" + where + ` ORDER BY created_at ${sortBy} LIMIT ? OFFSET ?`).all(...params, limit, offset);
|
|
457
565
|
const findMergeSources = db.prepare("SELECT id, summary, role FROM chunks WHERE dedup_target = ? AND (dedup_status = 'merged' OR dedup_status = 'duplicate')");
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
566
|
+
const chunkIds = rawMemories.map((m) => m.id);
|
|
567
|
+
const sharingMap = new Map();
|
|
568
|
+
const localShareMap = new Map();
|
|
569
|
+
if (chunkIds.length > 0) {
|
|
570
|
+
try {
|
|
571
|
+
const placeholders = chunkIds.map(() => "?").join(",");
|
|
572
|
+
const sharedRows = db.prepare(`SELECT source_chunk_id, visibility, group_id FROM hub_memories WHERE source_chunk_id IN (${placeholders})`).all(...chunkIds);
|
|
573
|
+
for (const r of sharedRows)
|
|
574
|
+
sharingMap.set(r.source_chunk_id, r);
|
|
575
|
+
const localRows = db.prepare(`SELECT chunk_id, original_owner, shared_at FROM local_shared_memories WHERE chunk_id IN (${placeholders})`).all(...chunkIds);
|
|
576
|
+
for (const r of localRows)
|
|
577
|
+
localShareMap.set(r.chunk_id, r);
|
|
578
|
+
}
|
|
579
|
+
catch {
|
|
461
580
|
}
|
|
462
|
-
|
|
581
|
+
}
|
|
582
|
+
const memories = rawMemories.map((m) => {
|
|
583
|
+
const out = m.role === "user" && m.content ? { ...m, content: (0, capture_1.stripInboundMetadata)(m.content) } : { ...m };
|
|
584
|
+
if (out.merge_count > 0) {
|
|
463
585
|
const sources = findMergeSources.all(m.id);
|
|
464
|
-
|
|
586
|
+
out.merge_sources = sources;
|
|
465
587
|
}
|
|
466
|
-
|
|
588
|
+
const shared = sharingMap.get(m.id);
|
|
589
|
+
const localShared = localShareMap.get(m.id);
|
|
590
|
+
out.sharingVisibility = shared?.visibility ?? null;
|
|
591
|
+
out.sharingGroupId = shared?.group_id ?? null;
|
|
592
|
+
out.localSharing = out.owner === "public";
|
|
593
|
+
out.localSharingManaged = !!localShared;
|
|
594
|
+
out.localOriginalOwner = localShared?.original_owner ?? null;
|
|
595
|
+
return out;
|
|
467
596
|
});
|
|
468
597
|
this.store.recordViewerEvent("list");
|
|
469
598
|
this.jsonResponse(res, {
|
|
@@ -477,19 +606,35 @@ class ViewerServer {
|
|
|
477
606
|
this.jsonResponse(res, data);
|
|
478
607
|
}
|
|
479
608
|
serveToolMetrics(res, url) {
|
|
480
|
-
const
|
|
609
|
+
const fromParam = url.searchParams.get("from");
|
|
610
|
+
const toParam = url.searchParams.get("to");
|
|
611
|
+
if (fromParam) {
|
|
612
|
+
const fromMs = new Date(fromParam).getTime();
|
|
613
|
+
const toMs = toParam ? new Date(toParam).getTime() : Date.now();
|
|
614
|
+
if (isNaN(fromMs) || isNaN(toMs)) {
|
|
615
|
+
this.jsonResponse(res, { error: "Invalid date" }, 400);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
const diffMin = Math.max(10, Math.min(43200, Math.round((toMs - fromMs) / 60000)));
|
|
619
|
+
const data = this.store.getToolMetrics(diffMin, fromMs, toMs);
|
|
620
|
+
this.jsonResponse(res, data);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
const minutes = Math.min(43200, Math.max(10, Number(url.searchParams.get("minutes")) || 60));
|
|
481
624
|
const data = this.store.getToolMetrics(minutes);
|
|
482
625
|
this.jsonResponse(res, data);
|
|
483
626
|
}
|
|
484
627
|
serveTasks(res, url) {
|
|
485
628
|
this.store.recordViewerEvent("tasks_list");
|
|
486
629
|
const status = url.searchParams.get("status") ?? undefined;
|
|
630
|
+
const owner = url.searchParams.get("owner") ?? undefined;
|
|
487
631
|
const limit = Math.min(100, Math.max(1, Number(url.searchParams.get("limit")) || 50));
|
|
488
632
|
const offset = Math.max(0, Number(url.searchParams.get("offset")) || 0);
|
|
489
|
-
const { tasks, total } = this.store.listTasks({ status, limit, offset });
|
|
633
|
+
const { tasks, total } = this.store.listTasks({ status, limit, offset, owner });
|
|
490
634
|
const db = this.store.db;
|
|
491
635
|
const items = tasks.map((t) => {
|
|
492
|
-
const meta = db.prepare("SELECT skill_status FROM tasks WHERE id = ?").get(t.id);
|
|
636
|
+
const meta = db.prepare("SELECT skill_status, owner FROM tasks WHERE id = ?").get(t.id);
|
|
637
|
+
const sharedTask = db.prepare("SELECT visibility FROM hub_tasks WHERE source_task_id = ? ORDER BY updated_at DESC LIMIT 1").get(t.id);
|
|
493
638
|
return {
|
|
494
639
|
id: t.id,
|
|
495
640
|
sessionKey: t.sessionKey,
|
|
@@ -500,6 +645,8 @@ class ViewerServer {
|
|
|
500
645
|
endedAt: t.endedAt,
|
|
501
646
|
chunkCount: this.store.countChunksByTask(t.id),
|
|
502
647
|
skillStatus: meta?.skill_status ?? null,
|
|
648
|
+
owner: meta?.owner ?? "agent:main",
|
|
649
|
+
sharingVisibility: sharedTask?.visibility ?? null,
|
|
503
650
|
};
|
|
504
651
|
});
|
|
505
652
|
this.jsonResponse(res, { tasks: items, total, limit, offset });
|
|
@@ -528,21 +675,26 @@ class ViewerServer {
|
|
|
528
675
|
}));
|
|
529
676
|
const db = this.store.db;
|
|
530
677
|
const meta = db.prepare("SELECT skill_status, skill_reason FROM tasks WHERE id = ?").get(taskId);
|
|
678
|
+
const sharedTask = db.prepare("SELECT visibility, group_id FROM hub_tasks WHERE source_task_id = ? ORDER BY updated_at DESC LIMIT 1").get(taskId);
|
|
531
679
|
this.jsonResponse(res, {
|
|
532
680
|
id: task.id,
|
|
533
681
|
sessionKey: task.sessionKey,
|
|
534
682
|
title: task.title,
|
|
535
683
|
summary: task.summary,
|
|
536
684
|
status: task.status,
|
|
685
|
+
owner: task.owner ?? "agent:main",
|
|
537
686
|
startedAt: task.startedAt,
|
|
538
687
|
endedAt: task.endedAt,
|
|
539
688
|
chunks: chunkItems,
|
|
540
689
|
skillStatus: meta?.skill_status ?? null,
|
|
541
690
|
skillReason: meta?.skill_reason ?? null,
|
|
542
691
|
skillLinks,
|
|
692
|
+
sharingVisibility: sharedTask?.visibility ?? null,
|
|
693
|
+
sharingGroupId: sharedTask?.group_id ?? null,
|
|
694
|
+
hubTaskId: sharedTask ? true : false,
|
|
543
695
|
});
|
|
544
696
|
}
|
|
545
|
-
serveStats(res) {
|
|
697
|
+
serveStats(res, url) {
|
|
546
698
|
const emptyStats = {
|
|
547
699
|
totalMemories: 0, totalSessions: 0, totalEmbeddings: 0, totalSkills: 0,
|
|
548
700
|
embeddingProvider: this.embedder?.provider ?? "none",
|
|
@@ -554,6 +706,7 @@ class ViewerServer {
|
|
|
554
706
|
this.jsonResponse(res, emptyStats);
|
|
555
707
|
return;
|
|
556
708
|
}
|
|
709
|
+
const ownerFilter = url?.searchParams.get("owner") ?? "";
|
|
557
710
|
try {
|
|
558
711
|
const db = this.store.db;
|
|
559
712
|
const total = db.prepare("SELECT COUNT(*) as count FROM chunks").get();
|
|
@@ -572,7 +725,22 @@ class ViewerServer {
|
|
|
572
725
|
embCount = db.prepare("SELECT COUNT(*) as count FROM embeddings").get().count;
|
|
573
726
|
}
|
|
574
727
|
catch { /* table may not exist */ }
|
|
575
|
-
|
|
728
|
+
let sessionQuery;
|
|
729
|
+
let sessionParams;
|
|
730
|
+
if (ownerFilter && ownerFilter.startsWith("agent:")) {
|
|
731
|
+
const agentPrefix = ownerFilter + ":";
|
|
732
|
+
sessionQuery = "SELECT session_key, COUNT(*) as count, MIN(created_at) as earliest, MAX(created_at) as latest FROM chunks WHERE (owner = ? OR (owner = 'public' AND session_key LIKE ?)) GROUP BY session_key ORDER BY latest DESC";
|
|
733
|
+
sessionParams = [ownerFilter, agentPrefix + "%"];
|
|
734
|
+
}
|
|
735
|
+
else if (ownerFilter) {
|
|
736
|
+
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";
|
|
737
|
+
sessionParams = [ownerFilter];
|
|
738
|
+
}
|
|
739
|
+
else {
|
|
740
|
+
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";
|
|
741
|
+
sessionParams = [];
|
|
742
|
+
}
|
|
743
|
+
const sessionList = db.prepare(sessionQuery).all(...sessionParams);
|
|
576
744
|
let skillCount = 0;
|
|
577
745
|
try {
|
|
578
746
|
skillCount = db.prepare("SELECT COUNT(*) as count FROM skills").get().count;
|
|
@@ -586,7 +754,7 @@ class ViewerServer {
|
|
|
586
754
|
catch { /* column may not exist yet */ }
|
|
587
755
|
let owners = [];
|
|
588
756
|
try {
|
|
589
|
-
const ownerRows = db.prepare("SELECT DISTINCT owner FROM chunks WHERE owner IS NOT NULL ORDER BY owner").all();
|
|
757
|
+
const ownerRows = db.prepare("SELECT DISTINCT owner FROM chunks WHERE owner IS NOT NULL AND owner LIKE 'agent:%' ORDER BY owner").all();
|
|
590
758
|
owners = ownerRows.map((o) => o.owner);
|
|
591
759
|
}
|
|
592
760
|
catch { /* column may not exist yet */ }
|
|
@@ -721,7 +889,12 @@ class ViewerServer {
|
|
|
721
889
|
if (visibility) {
|
|
722
890
|
skills = skills.filter(s => s.visibility === visibility);
|
|
723
891
|
}
|
|
724
|
-
|
|
892
|
+
const db = this.store.db;
|
|
893
|
+
const enriched = skills.map(s => {
|
|
894
|
+
const hub = db.prepare("SELECT visibility FROM hub_skills WHERE source_skill_id = ? ORDER BY updated_at DESC LIMIT 1").get(s.id);
|
|
895
|
+
return { ...s, sharingVisibility: hub?.visibility ?? null };
|
|
896
|
+
});
|
|
897
|
+
this.jsonResponse(res, { skills: enriched });
|
|
725
898
|
}
|
|
726
899
|
serveSkillDetail(res, urlPath) {
|
|
727
900
|
const skillId = urlPath.replace("/api/skill/", "");
|
|
@@ -734,8 +907,10 @@ class ViewerServer {
|
|
|
734
907
|
const versions = this.store.getSkillVersions(skillId);
|
|
735
908
|
const relatedTasks = this.store.getTasksBySkill(skillId);
|
|
736
909
|
const files = node_fs_1.default.existsSync(skill.dirPath) ? this.walkDir(skill.dirPath, skill.dirPath) : [];
|
|
910
|
+
const db = this.store.db;
|
|
911
|
+
const sharedSkill = db.prepare("SELECT visibility, group_id FROM hub_skills WHERE source_skill_id = ? ORDER BY updated_at DESC LIMIT 1").get(skillId);
|
|
737
912
|
this.jsonResponse(res, {
|
|
738
|
-
skill,
|
|
913
|
+
skill: { ...skill, sharingVisibility: sharedSkill?.visibility ?? null, sharingGroupId: sharedSkill?.group_id ?? null },
|
|
739
914
|
versions: versions.map(v => ({
|
|
740
915
|
id: v.id,
|
|
741
916
|
version: v.version,
|
|
@@ -844,7 +1019,7 @@ class ViewerServer {
|
|
|
844
1019
|
handleSkillVisibility(req, res, urlPath) {
|
|
845
1020
|
const segments = urlPath.split("/");
|
|
846
1021
|
const skillId = segments[segments.length - 2];
|
|
847
|
-
this.readBody(req, (body) => {
|
|
1022
|
+
this.readBody(req, async (body) => {
|
|
848
1023
|
try {
|
|
849
1024
|
const parsed = JSON.parse(body);
|
|
850
1025
|
const visibility = parsed.visibility;
|
|
@@ -860,7 +1035,47 @@ class ViewerServer {
|
|
|
860
1035
|
return;
|
|
861
1036
|
}
|
|
862
1037
|
this.store.setSkillVisibility(skillId, visibility);
|
|
863
|
-
|
|
1038
|
+
let hubSynced = false;
|
|
1039
|
+
const sharing = this.ctx?.config?.sharing;
|
|
1040
|
+
if (sharing?.enabled && this.ctx) {
|
|
1041
|
+
try {
|
|
1042
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1043
|
+
if (visibility === "public") {
|
|
1044
|
+
const bundle = (0, skill_sync_1.buildSkillBundleForHub)(this.store, skillId);
|
|
1045
|
+
const response = await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/publish", {
|
|
1046
|
+
method: "POST",
|
|
1047
|
+
body: JSON.stringify({ visibility: "public", groupId: null, metadata: bundle.metadata, bundle: bundle.bundle }),
|
|
1048
|
+
});
|
|
1049
|
+
if (hubClient.userId) {
|
|
1050
|
+
const existing = this.store.getHubSkillBySource(hubClient.userId, skillId);
|
|
1051
|
+
this.store.upsertHubSkill({
|
|
1052
|
+
id: response?.skillId ?? existing?.id ?? node_crypto_1.default.randomUUID(),
|
|
1053
|
+
sourceSkillId: skillId, sourceUserId: hubClient.userId,
|
|
1054
|
+
name: skill.name, description: skill.description, version: skill.version,
|
|
1055
|
+
groupId: null, visibility: "public",
|
|
1056
|
+
bundle: JSON.stringify(bundle.bundle), qualityScore: skill.qualityScore,
|
|
1057
|
+
createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(),
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
hubSynced = true;
|
|
1061
|
+
this.log.info(`Skill "${skill.name}" published to Hub`);
|
|
1062
|
+
}
|
|
1063
|
+
else {
|
|
1064
|
+
await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/unpublish", {
|
|
1065
|
+
method: "POST",
|
|
1066
|
+
body: JSON.stringify({ sourceSkillId: skillId }),
|
|
1067
|
+
});
|
|
1068
|
+
if (hubClient.userId)
|
|
1069
|
+
this.store.deleteHubSkillBySource(hubClient.userId, skillId);
|
|
1070
|
+
hubSynced = true;
|
|
1071
|
+
this.log.info(`Skill "${skill.name}" unpublished from Hub`);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
catch (hubErr) {
|
|
1075
|
+
this.log.warn(`Hub sync failed for skill visibility change: ${hubErr}`);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
this.jsonResponse(res, { ok: true, skillId, visibility, hubSynced });
|
|
864
1079
|
}
|
|
865
1080
|
catch (err) {
|
|
866
1081
|
const errMsg = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
|
|
@@ -997,7 +1212,15 @@ class ViewerServer {
|
|
|
997
1212
|
const cleaned = chunk.role === "user" && chunk.content
|
|
998
1213
|
? { ...chunk, content: (0, capture_1.stripInboundMetadata)(chunk.content) }
|
|
999
1214
|
: chunk;
|
|
1000
|
-
this.
|
|
1215
|
+
const localShared = this.store.getLocalSharedMemory(chunkId);
|
|
1216
|
+
this.jsonResponse(res, {
|
|
1217
|
+
memory: {
|
|
1218
|
+
...cleaned,
|
|
1219
|
+
localSharing: cleaned.owner === "public",
|
|
1220
|
+
localSharingManaged: !!localShared,
|
|
1221
|
+
localOriginalOwner: localShared?.originalOwner ?? null,
|
|
1222
|
+
},
|
|
1223
|
+
});
|
|
1001
1224
|
}
|
|
1002
1225
|
handleUpdate(req, res, urlPath) {
|
|
1003
1226
|
const chunkId = urlPath.replace("/api/memory/", "");
|
|
@@ -1032,6 +1255,348 @@ class ViewerServer {
|
|
|
1032
1255
|
res.end(JSON.stringify({ error: "Not found" }));
|
|
1033
1256
|
}
|
|
1034
1257
|
}
|
|
1258
|
+
handleMemoryLocalShare(req, res) {
|
|
1259
|
+
this.readBody(req, (body) => {
|
|
1260
|
+
try {
|
|
1261
|
+
const parsed = JSON.parse(body || "{}");
|
|
1262
|
+
const chunkId = String(parsed.chunkId || "");
|
|
1263
|
+
if (!chunkId)
|
|
1264
|
+
return this.jsonResponse(res, { ok: false, error: "missing_chunk_id" }, 400);
|
|
1265
|
+
const result = this.store.markMemorySharedLocally(chunkId);
|
|
1266
|
+
if (!result.ok) {
|
|
1267
|
+
return this.jsonResponse(res, { ok: false, error: result.reason ?? "share_failed" }, result.reason === "not_found" ? 404 : 400);
|
|
1268
|
+
}
|
|
1269
|
+
this.jsonResponse(res, {
|
|
1270
|
+
ok: true,
|
|
1271
|
+
chunkId,
|
|
1272
|
+
owner: result.owner,
|
|
1273
|
+
localSharing: true,
|
|
1274
|
+
localSharingManaged: true,
|
|
1275
|
+
localOriginalOwner: result.originalOwner ?? null,
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
catch (err) {
|
|
1279
|
+
this.jsonResponse(res, { ok: false, error: String(err) }, 400);
|
|
1280
|
+
}
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
handleMemoryLocalUnshare(req, res) {
|
|
1284
|
+
this.readBody(req, (body) => {
|
|
1285
|
+
try {
|
|
1286
|
+
const parsed = JSON.parse(body || "{}");
|
|
1287
|
+
const chunkId = String(parsed.chunkId || "");
|
|
1288
|
+
const privateOwner = typeof parsed.privateOwner === "string" ? parsed.privateOwner : undefined;
|
|
1289
|
+
if (!chunkId)
|
|
1290
|
+
return this.jsonResponse(res, { ok: false, error: "missing_chunk_id" }, 400);
|
|
1291
|
+
const result = this.store.unmarkMemorySharedLocally(chunkId, privateOwner);
|
|
1292
|
+
if (!result.ok) {
|
|
1293
|
+
return this.jsonResponse(res, { ok: false, error: result.reason ?? "unshare_failed" }, result.reason === "not_found" ? 404 : 400);
|
|
1294
|
+
}
|
|
1295
|
+
this.jsonResponse(res, {
|
|
1296
|
+
ok: true,
|
|
1297
|
+
chunkId,
|
|
1298
|
+
owner: result.owner,
|
|
1299
|
+
localSharing: false,
|
|
1300
|
+
localOriginalOwner: result.originalOwner ?? null,
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
catch (err) {
|
|
1304
|
+
this.jsonResponse(res, { ok: false, error: String(err) }, 400);
|
|
1305
|
+
}
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
// ─── Unified scope API ───
|
|
1309
|
+
handleMemoryScope(req, res, urlPath) {
|
|
1310
|
+
const chunkId = urlPath.split("/")[3];
|
|
1311
|
+
this.readBody(req, async (body) => {
|
|
1312
|
+
try {
|
|
1313
|
+
const parsed = JSON.parse(body || "{}");
|
|
1314
|
+
const scope = parsed.scope;
|
|
1315
|
+
if (!["private", "local", "team"].includes(scope)) {
|
|
1316
|
+
return this.jsonResponse(res, { ok: false, error: "scope must be 'private', 'local', or 'team'" }, 400);
|
|
1317
|
+
}
|
|
1318
|
+
const db = this.store.db;
|
|
1319
|
+
const chunk = db.prepare("SELECT * FROM chunks WHERE id = ?").get(chunkId);
|
|
1320
|
+
if (!chunk)
|
|
1321
|
+
return this.jsonResponse(res, { ok: false, error: "not_found" }, 404);
|
|
1322
|
+
if (chunk.dedup_status && chunk.dedup_status !== "active") {
|
|
1323
|
+
return this.jsonResponse(res, { ok: false, error: "inactive_memory", message: "Merged/duplicate memories cannot be shared" }, 400);
|
|
1324
|
+
}
|
|
1325
|
+
const isLocalShared = chunk.owner === "public";
|
|
1326
|
+
const hubMemory = this.getHubMemoryForChunk(chunkId);
|
|
1327
|
+
const isTeamShared = !!hubMemory;
|
|
1328
|
+
const currentScope = isTeamShared ? "team" : isLocalShared ? "local" : "private";
|
|
1329
|
+
if (scope === currentScope) {
|
|
1330
|
+
return this.jsonResponse(res, { ok: true, scope, changed: false });
|
|
1331
|
+
}
|
|
1332
|
+
let hubSynced = false;
|
|
1333
|
+
if (scope === "team") {
|
|
1334
|
+
if (!isLocalShared)
|
|
1335
|
+
this.store.markMemorySharedLocally(chunkId);
|
|
1336
|
+
if (!isTeamShared) {
|
|
1337
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1338
|
+
const refreshedChunk = db.prepare("SELECT * FROM chunks WHERE id = ?").get(chunkId);
|
|
1339
|
+
const response = await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/share", {
|
|
1340
|
+
method: "POST",
|
|
1341
|
+
body: JSON.stringify({ memory: { sourceChunkId: refreshedChunk.id, role: refreshedChunk.role, content: refreshedChunk.content, summary: refreshedChunk.summary, kind: refreshedChunk.kind, groupId: null, visibility: "public" } }),
|
|
1342
|
+
});
|
|
1343
|
+
if (hubClient.userId) {
|
|
1344
|
+
const existing = this.store.getHubMemoryBySource(hubClient.userId, chunkId);
|
|
1345
|
+
this.store.upsertHubMemory({
|
|
1346
|
+
id: response?.memoryId ?? existing?.id ?? node_crypto_1.default.randomUUID(),
|
|
1347
|
+
sourceChunkId: chunkId, sourceUserId: hubClient.userId,
|
|
1348
|
+
role: refreshedChunk.role, content: refreshedChunk.content, summary: refreshedChunk.summary ?? "",
|
|
1349
|
+
kind: refreshedChunk.kind, groupId: null, visibility: "public",
|
|
1350
|
+
createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(),
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
hubSynced = true;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
else if (scope === "local") {
|
|
1357
|
+
if (isTeamShared) {
|
|
1358
|
+
try {
|
|
1359
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1360
|
+
await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/unshare", {
|
|
1361
|
+
method: "POST", body: JSON.stringify({ sourceChunkId: chunkId }),
|
|
1362
|
+
});
|
|
1363
|
+
if (hubClient.userId)
|
|
1364
|
+
this.store.deleteHubMemoryBySource(hubClient.userId, chunkId);
|
|
1365
|
+
hubSynced = true;
|
|
1366
|
+
}
|
|
1367
|
+
catch (err) {
|
|
1368
|
+
this.log.warn(`Failed to unshare memory from team: ${err}`);
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
if (!isLocalShared)
|
|
1372
|
+
this.store.markMemorySharedLocally(chunkId);
|
|
1373
|
+
}
|
|
1374
|
+
else {
|
|
1375
|
+
if (isTeamShared) {
|
|
1376
|
+
try {
|
|
1377
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1378
|
+
await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/unshare", {
|
|
1379
|
+
method: "POST", body: JSON.stringify({ sourceChunkId: chunkId }),
|
|
1380
|
+
});
|
|
1381
|
+
if (hubClient.userId)
|
|
1382
|
+
this.store.deleteHubMemoryBySource(hubClient.userId, chunkId);
|
|
1383
|
+
hubSynced = true;
|
|
1384
|
+
}
|
|
1385
|
+
catch (err) {
|
|
1386
|
+
this.log.warn(`Failed to unshare memory from team: ${err}`);
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
if (isLocalShared)
|
|
1390
|
+
this.store.unmarkMemorySharedLocally(chunkId);
|
|
1391
|
+
}
|
|
1392
|
+
this.jsonResponse(res, { ok: true, scope, changed: true, hubSynced });
|
|
1393
|
+
}
|
|
1394
|
+
catch (err) {
|
|
1395
|
+
this.jsonResponse(res, { ok: false, error: String(err) }, 500);
|
|
1396
|
+
}
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
handleTaskScope(req, res, urlPath) {
|
|
1400
|
+
const taskId = urlPath.split("/")[3];
|
|
1401
|
+
this.readBody(req, async (body) => {
|
|
1402
|
+
try {
|
|
1403
|
+
const parsed = JSON.parse(body || "{}");
|
|
1404
|
+
const scope = parsed.scope;
|
|
1405
|
+
if (!["private", "local", "team"].includes(scope)) {
|
|
1406
|
+
return this.jsonResponse(res, { ok: false, error: "scope must be 'private', 'local', or 'team'" }, 400);
|
|
1407
|
+
}
|
|
1408
|
+
const task = this.store.getTask(taskId);
|
|
1409
|
+
if (!task)
|
|
1410
|
+
return this.jsonResponse(res, { ok: false, error: "task_not_found" }, 404);
|
|
1411
|
+
if (scope !== "private" && task.status !== "completed") {
|
|
1412
|
+
return this.jsonResponse(res, { ok: false, error: "only_completed_tasks_can_be_shared" }, 400);
|
|
1413
|
+
}
|
|
1414
|
+
const isLocalShared = task.owner === "public";
|
|
1415
|
+
const hubTask = this.getHubTaskForLocal(taskId);
|
|
1416
|
+
const isTeamShared = !!hubTask;
|
|
1417
|
+
const currentScope = isTeamShared ? "team" : isLocalShared ? "local" : "private";
|
|
1418
|
+
if (scope === currentScope) {
|
|
1419
|
+
return this.jsonResponse(res, { ok: true, scope, changed: false });
|
|
1420
|
+
}
|
|
1421
|
+
let hubSynced = false;
|
|
1422
|
+
if (scope === "local" || scope === "team") {
|
|
1423
|
+
if (!isLocalShared) {
|
|
1424
|
+
const originalOwner = task.owner;
|
|
1425
|
+
const db = this.store.db;
|
|
1426
|
+
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());
|
|
1427
|
+
db.prepare("UPDATE tasks SET owner = 'public' WHERE id = ?").run(taskId);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
if (scope === "team") {
|
|
1431
|
+
if (!isTeamShared) {
|
|
1432
|
+
const chunks = this.store.getChunksByTask(taskId);
|
|
1433
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1434
|
+
const refreshedTask = this.store.getTask(taskId);
|
|
1435
|
+
const response = await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/share", {
|
|
1436
|
+
method: "POST",
|
|
1437
|
+
body: JSON.stringify({
|
|
1438
|
+
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() },
|
|
1439
|
+
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() })),
|
|
1440
|
+
}),
|
|
1441
|
+
});
|
|
1442
|
+
if (hubClient.userId) {
|
|
1443
|
+
const existing = this.store.getHubTaskBySource(hubClient.userId, taskId);
|
|
1444
|
+
this.store.upsertHubTask({
|
|
1445
|
+
id: response?.taskId ?? existing?.id ?? node_crypto_1.default.randomUUID(),
|
|
1446
|
+
sourceTaskId: taskId, sourceUserId: hubClient.userId, title: refreshedTask.title ?? "",
|
|
1447
|
+
summary: refreshedTask.summary ?? "", groupId: null, visibility: "public",
|
|
1448
|
+
createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(),
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1451
|
+
hubSynced = true;
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
if (scope === "local" && isTeamShared) {
|
|
1455
|
+
try {
|
|
1456
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1457
|
+
await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/unshare", {
|
|
1458
|
+
method: "POST", body: JSON.stringify({ sourceTaskId: taskId }),
|
|
1459
|
+
});
|
|
1460
|
+
if (hubClient.userId)
|
|
1461
|
+
this.store.deleteHubTaskBySource(hubClient.userId, taskId);
|
|
1462
|
+
hubSynced = true;
|
|
1463
|
+
}
|
|
1464
|
+
catch (err) {
|
|
1465
|
+
this.log.warn(`Failed to unshare task from team: ${err}`);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
if (scope === "private") {
|
|
1469
|
+
if (isTeamShared) {
|
|
1470
|
+
try {
|
|
1471
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1472
|
+
await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/unshare", {
|
|
1473
|
+
method: "POST", body: JSON.stringify({ sourceTaskId: taskId }),
|
|
1474
|
+
});
|
|
1475
|
+
if (hubClient.userId)
|
|
1476
|
+
this.store.deleteHubTaskBySource(hubClient.userId, taskId);
|
|
1477
|
+
hubSynced = true;
|
|
1478
|
+
}
|
|
1479
|
+
catch (err) {
|
|
1480
|
+
this.log.warn(`Failed to unshare task from team: ${err}`);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
if (isLocalShared) {
|
|
1484
|
+
const db = this.store.db;
|
|
1485
|
+
const shared = db.prepare("SELECT original_owner FROM local_shared_tasks WHERE task_id = ?").get(taskId);
|
|
1486
|
+
const restoreOwner = shared?.original_owner ?? task.owner;
|
|
1487
|
+
if (restoreOwner && restoreOwner !== "public") {
|
|
1488
|
+
db.prepare("UPDATE tasks SET owner = ? WHERE id = ?").run(restoreOwner, taskId);
|
|
1489
|
+
}
|
|
1490
|
+
db.prepare("DELETE FROM local_shared_tasks WHERE task_id = ?").run(taskId);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
this.jsonResponse(res, { ok: true, scope, changed: true, hubSynced });
|
|
1494
|
+
}
|
|
1495
|
+
catch (err) {
|
|
1496
|
+
this.jsonResponse(res, { ok: false, error: String(err) }, 500);
|
|
1497
|
+
}
|
|
1498
|
+
});
|
|
1499
|
+
}
|
|
1500
|
+
handleSkillScope(req, res, urlPath) {
|
|
1501
|
+
const skillId = urlPath.split("/")[3];
|
|
1502
|
+
this.readBody(req, async (body) => {
|
|
1503
|
+
try {
|
|
1504
|
+
const parsed = JSON.parse(body || "{}");
|
|
1505
|
+
const scope = parsed.scope;
|
|
1506
|
+
if (!["private", "local", "team"].includes(scope)) {
|
|
1507
|
+
return this.jsonResponse(res, { ok: false, error: "scope must be 'private', 'local', or 'team'" }, 400);
|
|
1508
|
+
}
|
|
1509
|
+
const skill = this.store.getSkill(skillId);
|
|
1510
|
+
if (!skill)
|
|
1511
|
+
return this.jsonResponse(res, { ok: false, error: "skill_not_found" }, 404);
|
|
1512
|
+
if (scope !== "private" && skill.status !== "active") {
|
|
1513
|
+
return this.jsonResponse(res, { ok: false, error: "only_active_skills_can_be_shared" }, 400);
|
|
1514
|
+
}
|
|
1515
|
+
const isLocalShared = skill.visibility === "public";
|
|
1516
|
+
const hubSkill = this.getHubSkillForLocal(skillId);
|
|
1517
|
+
const isTeamShared = !!hubSkill;
|
|
1518
|
+
const currentScope = isTeamShared ? "team" : isLocalShared ? "local" : "private";
|
|
1519
|
+
if (scope === currentScope) {
|
|
1520
|
+
return this.jsonResponse(res, { ok: true, scope, changed: false });
|
|
1521
|
+
}
|
|
1522
|
+
let hubSynced = false;
|
|
1523
|
+
if (scope === "local" || scope === "team") {
|
|
1524
|
+
if (!isLocalShared)
|
|
1525
|
+
this.store.setSkillVisibility(skillId, "public");
|
|
1526
|
+
}
|
|
1527
|
+
if (scope === "team") {
|
|
1528
|
+
if (!isTeamShared) {
|
|
1529
|
+
const bundle = (0, skill_sync_1.buildSkillBundleForHub)(this.store, skillId);
|
|
1530
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1531
|
+
const response = await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/publish", {
|
|
1532
|
+
method: "POST",
|
|
1533
|
+
body: JSON.stringify({ visibility: "public", groupId: null, metadata: bundle.metadata, bundle: bundle.bundle }),
|
|
1534
|
+
});
|
|
1535
|
+
if (hubClient.userId) {
|
|
1536
|
+
const existing = this.store.getHubSkillBySource(hubClient.userId, skillId);
|
|
1537
|
+
this.store.upsertHubSkill({
|
|
1538
|
+
id: response?.skillId ?? existing?.id ?? node_crypto_1.default.randomUUID(),
|
|
1539
|
+
sourceSkillId: skillId, sourceUserId: hubClient.userId,
|
|
1540
|
+
name: skill.name, description: skill.description, version: skill.version,
|
|
1541
|
+
groupId: null, visibility: "public",
|
|
1542
|
+
bundle: JSON.stringify(bundle.bundle), qualityScore: skill.qualityScore,
|
|
1543
|
+
createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(),
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
hubSynced = true;
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
if (scope === "local" && isTeamShared) {
|
|
1550
|
+
try {
|
|
1551
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1552
|
+
await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/unpublish", {
|
|
1553
|
+
method: "POST", body: JSON.stringify({ sourceSkillId: skillId }),
|
|
1554
|
+
});
|
|
1555
|
+
if (hubClient.userId)
|
|
1556
|
+
this.store.deleteHubSkillBySource(hubClient.userId, skillId);
|
|
1557
|
+
hubSynced = true;
|
|
1558
|
+
}
|
|
1559
|
+
catch (err) {
|
|
1560
|
+
this.log.warn(`Failed to unpublish skill from team: ${err}`);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
if (scope === "private") {
|
|
1564
|
+
if (isTeamShared) {
|
|
1565
|
+
try {
|
|
1566
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1567
|
+
await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/unpublish", {
|
|
1568
|
+
method: "POST", body: JSON.stringify({ sourceSkillId: skillId }),
|
|
1569
|
+
});
|
|
1570
|
+
if (hubClient.userId)
|
|
1571
|
+
this.store.deleteHubSkillBySource(hubClient.userId, skillId);
|
|
1572
|
+
hubSynced = true;
|
|
1573
|
+
}
|
|
1574
|
+
catch (err) {
|
|
1575
|
+
this.log.warn(`Failed to unpublish skill from team: ${err}`);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
if (isLocalShared)
|
|
1579
|
+
this.store.setSkillVisibility(skillId, "private");
|
|
1580
|
+
}
|
|
1581
|
+
this.jsonResponse(res, { ok: true, scope, changed: true, hubSynced });
|
|
1582
|
+
}
|
|
1583
|
+
catch (err) {
|
|
1584
|
+
this.jsonResponse(res, { ok: false, error: String(err) }, 500);
|
|
1585
|
+
}
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
getHubMemoryForChunk(chunkId) {
|
|
1589
|
+
const db = this.store.db;
|
|
1590
|
+
return db.prepare("SELECT * FROM hub_memories WHERE source_chunk_id = ? LIMIT 1").get(chunkId);
|
|
1591
|
+
}
|
|
1592
|
+
getHubTaskForLocal(taskId) {
|
|
1593
|
+
const db = this.store.db;
|
|
1594
|
+
return db.prepare("SELECT * FROM hub_tasks WHERE source_task_id = ? LIMIT 1").get(taskId);
|
|
1595
|
+
}
|
|
1596
|
+
getHubSkillForLocal(skillId) {
|
|
1597
|
+
const db = this.store.db;
|
|
1598
|
+
return db.prepare("SELECT * FROM hub_skills WHERE source_skill_id = ? LIMIT 1").get(skillId);
|
|
1599
|
+
}
|
|
1035
1600
|
handleDeleteSession(res, url) {
|
|
1036
1601
|
const key = url.searchParams.get("key");
|
|
1037
1602
|
if (!key) {
|
|
@@ -1072,81 +1637,1270 @@ class ViewerServer {
|
|
|
1072
1637
|
const ocHome = process.env.OPENCLAW_STATE_DIR || node_path_1.default.join(home, ".openclaw");
|
|
1073
1638
|
return node_path_1.default.join(ocHome, "openclaw.json");
|
|
1074
1639
|
}
|
|
1075
|
-
|
|
1640
|
+
getPluginEntryConfig(raw) {
|
|
1641
|
+
const entries = raw?.plugins?.entries ?? {};
|
|
1642
|
+
return entries["memos-local-openclaw-plugin"]?.config
|
|
1643
|
+
?? entries["memos-lite-openclaw-plugin"]?.config
|
|
1644
|
+
?? entries["memos-lite"]?.config
|
|
1645
|
+
?? {};
|
|
1646
|
+
}
|
|
1647
|
+
getResolvedViewerConfig(raw) {
|
|
1648
|
+
const pluginCfg = this.getPluginEntryConfig(raw);
|
|
1649
|
+
const stateDir = this.ctx?.stateDir ?? this.getOpenClawHome();
|
|
1650
|
+
return (0, config_1.resolveConfig)(pluginCfg, stateDir);
|
|
1651
|
+
}
|
|
1652
|
+
hasUsableEmbeddingProvider(cfg) {
|
|
1653
|
+
const embedding = cfg.embedding;
|
|
1654
|
+
if (!embedding?.provider)
|
|
1655
|
+
return false;
|
|
1656
|
+
if (embedding.provider === "openclaw") {
|
|
1657
|
+
return !!(this.ctx?.openclawAPI) && embedding.capabilities?.hostEmbedding === true;
|
|
1658
|
+
}
|
|
1659
|
+
return true;
|
|
1660
|
+
}
|
|
1661
|
+
hasUsableSummarizerProvider(cfg) {
|
|
1662
|
+
const summarizer = cfg.summarizer;
|
|
1663
|
+
if (!summarizer?.provider)
|
|
1664
|
+
return false;
|
|
1665
|
+
if (summarizer.provider === "openclaw") {
|
|
1666
|
+
return !!(this.ctx?.openclawAPI) && summarizer.capabilities?.hostCompletion === true;
|
|
1667
|
+
}
|
|
1668
|
+
return true;
|
|
1669
|
+
}
|
|
1670
|
+
async serveSharingStatus(res) {
|
|
1671
|
+
const sharing = this.ctx?.config?.sharing;
|
|
1672
|
+
const persisted = this.store.getClientHubConnection();
|
|
1673
|
+
const resolvedHubUrl = sharing?.client?.hubAddress ? (0, hub_1.normalizeHubUrl)(sharing.client.hubAddress) : persisted?.hubUrl ?? null;
|
|
1674
|
+
const hasClientConfig = Boolean((sharing?.client?.hubAddress && sharing?.client?.userToken) ||
|
|
1675
|
+
(persisted?.hubUrl && persisted?.userToken));
|
|
1676
|
+
const base = {
|
|
1677
|
+
enabled: Boolean(sharing?.enabled),
|
|
1678
|
+
role: sharing?.role ?? null,
|
|
1679
|
+
clientConfigured: hasClientConfig,
|
|
1680
|
+
hubUrl: resolvedHubUrl,
|
|
1681
|
+
connection: { connected: false, user: null, hubUrl: undefined, teamName: null, apiVersion: null },
|
|
1682
|
+
admin: { canManageUsers: false, rejectSupported: false },
|
|
1683
|
+
};
|
|
1684
|
+
if (!this.ctx || !sharing?.enabled) {
|
|
1685
|
+
this.jsonResponse(res, base);
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
// Hub 模式下,本机就是管理者,直接赋予 admin 权限
|
|
1689
|
+
if (sharing.role === "hub") {
|
|
1690
|
+
base.admin.canManageUsers = true;
|
|
1691
|
+
base.admin.rejectSupported = true;
|
|
1692
|
+
base.connection.connected = true;
|
|
1693
|
+
base.connection.hubUrl = resolvedHubUrl ?? undefined;
|
|
1694
|
+
let adminUser = { username: "hub-admin", role: "admin" };
|
|
1695
|
+
try {
|
|
1696
|
+
const hub = this.resolveHubConnection();
|
|
1697
|
+
if (hub) {
|
|
1698
|
+
const me = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/me", { method: "GET" });
|
|
1699
|
+
if (me) {
|
|
1700
|
+
adminUser = {
|
|
1701
|
+
id: me.id,
|
|
1702
|
+
username: me.username ?? "hub-admin",
|
|
1703
|
+
role: me.role ?? "admin",
|
|
1704
|
+
};
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
catch { /* fallback to default */ }
|
|
1709
|
+
base.connection.user = adminUser;
|
|
1710
|
+
// Fetch team info from own hub
|
|
1711
|
+
try {
|
|
1712
|
+
const selfUrl = resolvedHubUrl || `http://localhost:${sharing.hub?.port ?? 21816}`;
|
|
1713
|
+
const info = await fetch(`${selfUrl}/api/v1/hub/info`).then(r => r.ok ? r.json() : null).catch(() => null);
|
|
1714
|
+
base.connection.teamName = info?.teamName ?? sharing.hub?.teamName ?? null;
|
|
1715
|
+
base.connection.apiVersion = info?.apiVersion ?? null;
|
|
1716
|
+
}
|
|
1717
|
+
catch { /* ignore */ }
|
|
1718
|
+
this.jsonResponse(res, base);
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
const hasPendingConnection = Boolean(persisted?.hubUrl && persisted?.userId && !persisted?.userToken);
|
|
1722
|
+
if (!hasClientConfig && !hasPendingConnection) {
|
|
1723
|
+
this.jsonResponse(res, base);
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1076
1726
|
try {
|
|
1077
|
-
const
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1727
|
+
const status = await (0, connector_1.getHubStatus)(this.store, this.ctx.config);
|
|
1728
|
+
const output = { ...base, connection: { ...base.connection, ...status } };
|
|
1729
|
+
if (status.user?.status === "pending") {
|
|
1730
|
+
output.connection.pendingApproval = true;
|
|
1081
1731
|
}
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
const pluginEntry = entries["memos-local-openclaw-plugin"]?.config
|
|
1085
|
-
?? entries["memos-local"]?.config
|
|
1086
|
-
?? entries["memos-lite-openclaw-plugin"]?.config
|
|
1087
|
-
?? entries["memos-lite"]?.config
|
|
1088
|
-
?? {};
|
|
1089
|
-
const result = { ...pluginEntry };
|
|
1090
|
-
const topEntry = entries["memos-local-openclaw-plugin"]
|
|
1091
|
-
?? entries["memos-local"]
|
|
1092
|
-
?? entries["memos-lite-openclaw-plugin"]
|
|
1093
|
-
?? entries["memos-lite"]
|
|
1094
|
-
?? {};
|
|
1095
|
-
if (pluginEntry.viewerPort == null && topEntry.viewerPort) {
|
|
1096
|
-
result.viewerPort = topEntry.viewerPort;
|
|
1732
|
+
if (status.user?.status === "rejected") {
|
|
1733
|
+
output.connection.rejected = true;
|
|
1097
1734
|
}
|
|
1098
|
-
|
|
1735
|
+
if (status.connected && status.hubUrl) {
|
|
1736
|
+
try {
|
|
1737
|
+
const info = await fetch(`${status.hubUrl}/api/v1/hub/info`).then((r) => (r.ok ? r.json() : null)).catch(() => null);
|
|
1738
|
+
output.connection.teamName = info?.teamName ?? null;
|
|
1739
|
+
output.connection.apiVersion = info?.apiVersion ?? null;
|
|
1740
|
+
}
|
|
1741
|
+
catch { }
|
|
1742
|
+
}
|
|
1743
|
+
else if (status.hubUrl) {
|
|
1744
|
+
try {
|
|
1745
|
+
const info = await fetch(`${status.hubUrl}/api/v1/hub/info`).then((r) => (r.ok ? r.json() : null)).catch(() => null);
|
|
1746
|
+
output.connection.teamName = info?.teamName ?? null;
|
|
1747
|
+
}
|
|
1748
|
+
catch { }
|
|
1749
|
+
}
|
|
1750
|
+
output.admin.canManageUsers = status.connected && status.user?.role === "admin";
|
|
1751
|
+
output.admin.rejectSupported = output.admin.canManageUsers;
|
|
1752
|
+
this.jsonResponse(res, output);
|
|
1099
1753
|
}
|
|
1100
|
-
catch (
|
|
1101
|
-
this.
|
|
1102
|
-
this.jsonResponse(res, {});
|
|
1754
|
+
catch (err) {
|
|
1755
|
+
this.jsonResponse(res, { ...base, error: String(err) });
|
|
1103
1756
|
}
|
|
1104
1757
|
}
|
|
1105
|
-
|
|
1106
|
-
this.
|
|
1758
|
+
async serveSharingPendingUsers(res) {
|
|
1759
|
+
if (!this.ctx)
|
|
1760
|
+
return this.jsonResponse(res, { users: [], error: "sharing_unavailable" });
|
|
1761
|
+
try {
|
|
1762
|
+
const hub = this.resolveHubConnection();
|
|
1763
|
+
if (!hub)
|
|
1764
|
+
return this.jsonResponse(res, { users: [], error: "not_configured" });
|
|
1765
|
+
const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/pending-users", { method: "GET" });
|
|
1766
|
+
this.jsonResponse(res, { users: Array.isArray(data?.users) ? data.users : [] });
|
|
1767
|
+
}
|
|
1768
|
+
catch (err) {
|
|
1769
|
+
this.jsonResponse(res, { users: [], error: String(err) });
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
handleSharingApproveUser(req, res) {
|
|
1773
|
+
this.readBody(req, async (body) => {
|
|
1774
|
+
if (!this.ctx)
|
|
1775
|
+
return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
1107
1776
|
try {
|
|
1108
|
-
const
|
|
1109
|
-
const
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
if (!plugins.entries)
|
|
1118
|
-
plugins.entries = {};
|
|
1119
|
-
const entries = plugins.entries;
|
|
1120
|
-
const entryKey = entries["memos-local-openclaw-plugin"] ? "memos-local-openclaw-plugin"
|
|
1121
|
-
: entries["memos-local"] ? "memos-local"
|
|
1122
|
-
: entries["memos-lite-openclaw-plugin"] ? "memos-lite-openclaw-plugin"
|
|
1123
|
-
: entries["memos-lite"] ? "memos-lite"
|
|
1124
|
-
: "memos-local-openclaw-plugin";
|
|
1125
|
-
if (!entries[entryKey])
|
|
1126
|
-
entries[entryKey] = { enabled: true };
|
|
1127
|
-
const entry = entries[entryKey];
|
|
1128
|
-
if (!entry.config)
|
|
1129
|
-
entry.config = {};
|
|
1130
|
-
const config = entry.config;
|
|
1131
|
-
if (newCfg.embedding)
|
|
1132
|
-
config.embedding = newCfg.embedding;
|
|
1133
|
-
if (newCfg.summarizer)
|
|
1134
|
-
config.summarizer = newCfg.summarizer;
|
|
1135
|
-
if (newCfg.skillEvolution)
|
|
1136
|
-
config.skillEvolution = newCfg.skillEvolution;
|
|
1137
|
-
if (newCfg.viewerPort)
|
|
1138
|
-
config.viewerPort = newCfg.viewerPort;
|
|
1139
|
-
if (newCfg.telemetry !== undefined)
|
|
1140
|
-
config.telemetry = newCfg.telemetry;
|
|
1141
|
-
node_fs_1.default.mkdirSync(node_path_1.default.dirname(cfgPath), { recursive: true });
|
|
1142
|
-
node_fs_1.default.writeFileSync(cfgPath, JSON.stringify(raw, null, 2), "utf-8");
|
|
1143
|
-
this.log.info("Plugin config updated via Viewer");
|
|
1144
|
-
this.jsonResponse(res, { ok: true });
|
|
1777
|
+
const parsed = JSON.parse(body || "{}");
|
|
1778
|
+
const hub = this.resolveHubConnection();
|
|
1779
|
+
if (!hub)
|
|
1780
|
+
return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1781
|
+
const result = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/approve-user", {
|
|
1782
|
+
method: "POST",
|
|
1783
|
+
body: JSON.stringify({ userId: parsed.userId, username: parsed.username }),
|
|
1784
|
+
});
|
|
1785
|
+
this.jsonResponse(res, { ok: true, result });
|
|
1145
1786
|
}
|
|
1146
|
-
catch (
|
|
1147
|
-
this.
|
|
1148
|
-
|
|
1149
|
-
|
|
1787
|
+
catch (err) {
|
|
1788
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1789
|
+
}
|
|
1790
|
+
});
|
|
1791
|
+
}
|
|
1792
|
+
handleSharingRejectUser(req, res) {
|
|
1793
|
+
this.readBody(req, async (body) => {
|
|
1794
|
+
if (!this.ctx)
|
|
1795
|
+
return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
1796
|
+
try {
|
|
1797
|
+
const parsed = JSON.parse(body || "{}");
|
|
1798
|
+
const hub = this.resolveHubConnection();
|
|
1799
|
+
if (!hub)
|
|
1800
|
+
return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1801
|
+
const result = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/reject-user", {
|
|
1802
|
+
method: "POST",
|
|
1803
|
+
body: JSON.stringify({ userId: parsed.userId }),
|
|
1804
|
+
});
|
|
1805
|
+
this.jsonResponse(res, { ok: true, result });
|
|
1806
|
+
}
|
|
1807
|
+
catch (err) {
|
|
1808
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1809
|
+
}
|
|
1810
|
+
});
|
|
1811
|
+
}
|
|
1812
|
+
handleSharingChangeRole(req, res) {
|
|
1813
|
+
this.readBody(req, async (body) => {
|
|
1814
|
+
if (!this.ctx)
|
|
1815
|
+
return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
1816
|
+
try {
|
|
1817
|
+
const parsed = JSON.parse(body || "{}");
|
|
1818
|
+
const hub = this.resolveHubConnection();
|
|
1819
|
+
if (!hub)
|
|
1820
|
+
return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1821
|
+
const result = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/change-role", {
|
|
1822
|
+
method: "POST",
|
|
1823
|
+
body: JSON.stringify({ userId: parsed.userId, role: parsed.role }),
|
|
1824
|
+
});
|
|
1825
|
+
this.jsonResponse(res, { ok: true, result });
|
|
1826
|
+
}
|
|
1827
|
+
catch (err) {
|
|
1828
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1829
|
+
}
|
|
1830
|
+
});
|
|
1831
|
+
}
|
|
1832
|
+
handleSharingRemoveUser(req, res) {
|
|
1833
|
+
this.readBody(req, async (body) => {
|
|
1834
|
+
if (!this.ctx)
|
|
1835
|
+
return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
1836
|
+
try {
|
|
1837
|
+
const parsed = JSON.parse(body || "{}");
|
|
1838
|
+
const hub = this.resolveHubConnection();
|
|
1839
|
+
if (!hub)
|
|
1840
|
+
return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1841
|
+
const result = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/remove-user", {
|
|
1842
|
+
method: "POST",
|
|
1843
|
+
body: JSON.stringify({ userId: parsed.userId, cleanResources: parsed.cleanResources === true }),
|
|
1844
|
+
});
|
|
1845
|
+
this.jsonResponse(res, { ok: true, result });
|
|
1846
|
+
}
|
|
1847
|
+
catch (err) {
|
|
1848
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1849
|
+
}
|
|
1850
|
+
});
|
|
1851
|
+
}
|
|
1852
|
+
handleAdminRenameUser(req, res) {
|
|
1853
|
+
this.readBody(req, async (body) => {
|
|
1854
|
+
if (!this.ctx)
|
|
1855
|
+
return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
1856
|
+
try {
|
|
1857
|
+
const parsed = JSON.parse(body || "{}");
|
|
1858
|
+
const hub = this.resolveHubConnection();
|
|
1859
|
+
if (!hub)
|
|
1860
|
+
return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1861
|
+
const result = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/rename-user", {
|
|
1862
|
+
method: "POST",
|
|
1863
|
+
body: JSON.stringify({ userId: parsed.userId, username: parsed.username }),
|
|
1864
|
+
});
|
|
1865
|
+
this.jsonResponse(res, { ok: true, result });
|
|
1866
|
+
}
|
|
1867
|
+
catch (err) {
|
|
1868
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1869
|
+
}
|
|
1870
|
+
});
|
|
1871
|
+
}
|
|
1872
|
+
handleRetryJoin(req, res) {
|
|
1873
|
+
this.readBody(req, async (_body) => {
|
|
1874
|
+
if (!this.ctx)
|
|
1875
|
+
return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
1876
|
+
const sharing = this.ctx.config.sharing;
|
|
1877
|
+
if (!sharing?.enabled || sharing.role !== "client") {
|
|
1878
|
+
return this.jsonResponse(res, { ok: false, error: "not_in_client_mode" });
|
|
1879
|
+
}
|
|
1880
|
+
const hubAddress = sharing.client?.hubAddress ?? "";
|
|
1881
|
+
const teamToken = sharing.client?.teamToken ?? "";
|
|
1882
|
+
if (!hubAddress || !teamToken) {
|
|
1883
|
+
return this.jsonResponse(res, { ok: false, error: "missing_hub_address_or_team_token" });
|
|
1884
|
+
}
|
|
1885
|
+
try {
|
|
1886
|
+
const hubUrl = (0, hub_1.normalizeHubUrl)(hubAddress);
|
|
1887
|
+
const os = await Promise.resolve().then(() => __importStar(require("os")));
|
|
1888
|
+
const nickname = sharing.client?.nickname;
|
|
1889
|
+
const username = nickname || os.userInfo().username || "user";
|
|
1890
|
+
const hostname = os.hostname() || "unknown";
|
|
1891
|
+
const result = await (0, hub_1.hubRequestJson)(hubUrl, "", "/api/v1/hub/join", {
|
|
1892
|
+
method: "POST",
|
|
1893
|
+
body: JSON.stringify({ teamToken, username, deviceName: hostname }),
|
|
1894
|
+
});
|
|
1895
|
+
this.store.setClientHubConnection({
|
|
1896
|
+
hubUrl,
|
|
1897
|
+
userId: String(result.userId || ""),
|
|
1898
|
+
username,
|
|
1899
|
+
userToken: result.userToken || "",
|
|
1900
|
+
role: "member",
|
|
1901
|
+
connectedAt: Date.now(),
|
|
1902
|
+
});
|
|
1903
|
+
this.jsonResponse(res, { ok: true, status: result.status || "pending" });
|
|
1904
|
+
}
|
|
1905
|
+
catch (err) {
|
|
1906
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1907
|
+
}
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1910
|
+
async serveSharingMemoryList(res, url) {
|
|
1911
|
+
if (!this.ctx)
|
|
1912
|
+
return this.jsonResponse(res, { memories: [], error: "sharing_unavailable" });
|
|
1913
|
+
try {
|
|
1914
|
+
const limit = Number(url.searchParams.get("limit") || 40);
|
|
1915
|
+
const hub = this.resolveHubConnection();
|
|
1916
|
+
let data;
|
|
1917
|
+
if (hub) {
|
|
1918
|
+
data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/memories?limit=${limit}`);
|
|
1919
|
+
}
|
|
1920
|
+
else {
|
|
1921
|
+
data = await (0, hub_1.hubListMemories)(this.store, this.ctx, { limit });
|
|
1922
|
+
}
|
|
1923
|
+
this.jsonResponse(res, { memories: Array.isArray(data?.memories) ? data.memories : [] });
|
|
1924
|
+
}
|
|
1925
|
+
catch (err) {
|
|
1926
|
+
this.jsonResponse(res, { memories: [], error: String(err) });
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
async serveSharingTaskList(res, url) {
|
|
1930
|
+
if (!this.ctx)
|
|
1931
|
+
return this.jsonResponse(res, { tasks: [], error: "sharing_unavailable" });
|
|
1932
|
+
try {
|
|
1933
|
+
const limit = Number(url.searchParams.get("limit") || 40);
|
|
1934
|
+
const hub = this.resolveHubConnection();
|
|
1935
|
+
let data;
|
|
1936
|
+
if (hub) {
|
|
1937
|
+
data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/tasks?limit=${limit}`);
|
|
1938
|
+
}
|
|
1939
|
+
else {
|
|
1940
|
+
data = await (0, hub_1.hubListTasks)(this.store, this.ctx, { limit });
|
|
1941
|
+
}
|
|
1942
|
+
this.jsonResponse(res, { tasks: Array.isArray(data?.tasks) ? data.tasks : [] });
|
|
1943
|
+
}
|
|
1944
|
+
catch (err) {
|
|
1945
|
+
this.jsonResponse(res, { tasks: [], error: String(err) });
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
async serveSharingSkillList(res, url) {
|
|
1949
|
+
if (!this.ctx)
|
|
1950
|
+
return this.jsonResponse(res, { skills: [], error: "sharing_unavailable" });
|
|
1951
|
+
try {
|
|
1952
|
+
const limit = Number(url.searchParams.get("limit") || 40);
|
|
1953
|
+
const hub = this.resolveHubConnection();
|
|
1954
|
+
let data;
|
|
1955
|
+
if (hub) {
|
|
1956
|
+
data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/skills/list?limit=${limit}`);
|
|
1957
|
+
}
|
|
1958
|
+
else {
|
|
1959
|
+
data = await (0, hub_1.hubListSkills)(this.store, this.ctx, { limit });
|
|
1960
|
+
}
|
|
1961
|
+
this.jsonResponse(res, { skills: Array.isArray(data?.skills) ? data.skills : [] });
|
|
1962
|
+
}
|
|
1963
|
+
catch (err) {
|
|
1964
|
+
this.jsonResponse(res, { skills: [], error: String(err) });
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
handleSharingMemorySearch(req, res) {
|
|
1968
|
+
this.readBody(req, async (body) => {
|
|
1969
|
+
if (!this.ctx)
|
|
1970
|
+
return this.jsonResponse(res, { local: { hits: [], meta: {} }, hub: { hits: [], meta: { totalCandidates: 0, searchedGroups: [], includedPublic: false } }, error: "sharing_unavailable" });
|
|
1971
|
+
const emptyHub = { hits: [], meta: { totalCandidates: 0, searchedGroups: [], includedPublic: false } };
|
|
1972
|
+
try {
|
|
1973
|
+
const parsed = JSON.parse(body || "{}");
|
|
1974
|
+
const query = String(parsed.query || "");
|
|
1975
|
+
const role = typeof parsed.role === "string" ? parsed.role : undefined;
|
|
1976
|
+
const maxResults = typeof parsed.maxResults === "number" ? parsed.maxResults : 10;
|
|
1977
|
+
const scope = parsed.scope === "group" || parsed.scope === "all" || parsed.scope === "hub" ? (parsed.scope === "hub" ? "all" : parsed.scope) : "local";
|
|
1978
|
+
const local = this.searchLocalViewerMemories(query, { role, maxResults });
|
|
1979
|
+
if (scope === "local") {
|
|
1980
|
+
return this.jsonResponse(res, { local: { hits: local.hits, meta: local.meta }, hub: emptyHub });
|
|
1981
|
+
}
|
|
1982
|
+
try {
|
|
1983
|
+
const conn = this.resolveHubConnection();
|
|
1984
|
+
let hub;
|
|
1985
|
+
if (conn) {
|
|
1986
|
+
hub = await (0, hub_1.hubRequestJson)(conn.hubUrl, conn.userToken, "/api/v1/hub/search", {
|
|
1987
|
+
method: "POST", body: JSON.stringify({ query, maxResults, scope }),
|
|
1988
|
+
});
|
|
1989
|
+
}
|
|
1990
|
+
else {
|
|
1991
|
+
hub = await (0, hub_1.hubSearchMemories)(this.store, this.ctx, { query, maxResults, scope });
|
|
1992
|
+
}
|
|
1993
|
+
this.jsonResponse(res, { local: { hits: local.hits, meta: local.meta }, hub });
|
|
1994
|
+
}
|
|
1995
|
+
catch (err) {
|
|
1996
|
+
this.jsonResponse(res, { local: { hits: local.hits, meta: local.meta }, hub: emptyHub, error: String(err) });
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
catch (err) {
|
|
2000
|
+
this.jsonResponse(res, { local: { hits: [], meta: {} }, hub: emptyHub, error: String(err) });
|
|
2001
|
+
}
|
|
2002
|
+
});
|
|
2003
|
+
}
|
|
2004
|
+
handleSharingMemoryDetail(req, res) {
|
|
2005
|
+
this.readBody(req, async (body) => {
|
|
2006
|
+
if (!this.ctx)
|
|
2007
|
+
return this.jsonResponse(res, { error: "sharing_unavailable" });
|
|
2008
|
+
try {
|
|
2009
|
+
const parsed = JSON.parse(body || "{}");
|
|
2010
|
+
const detail = await (0, hub_1.hubGetMemoryDetail)(this.store, this.ctx, { remoteHitId: String(parsed.remoteHitId || "") });
|
|
2011
|
+
this.jsonResponse(res, detail);
|
|
2012
|
+
}
|
|
2013
|
+
catch (err) {
|
|
2014
|
+
this.jsonResponse(res, { error: String(err) });
|
|
2015
|
+
}
|
|
2016
|
+
});
|
|
2017
|
+
}
|
|
2018
|
+
async serveSharingSkillSearch(res, url) {
|
|
2019
|
+
if (!this.ctx)
|
|
2020
|
+
return this.jsonResponse(res, { local: { hits: [] }, hub: { hits: [] }, error: "sharing_unavailable" });
|
|
2021
|
+
try {
|
|
2022
|
+
const query = String(url.searchParams.get("query") || "");
|
|
2023
|
+
const scope = url.searchParams.get("scope") === "group" || url.searchParams.get("scope") === "all" ? url.searchParams.get("scope") : "local";
|
|
2024
|
+
const recall = new engine_1.RecallEngine(this.store, this.embedder, this.ctx);
|
|
2025
|
+
const localHits = await recall.searchSkills(query, "mix", "agent:main");
|
|
2026
|
+
if (scope === "local") {
|
|
2027
|
+
return this.jsonResponse(res, { local: { hits: localHits }, hub: { hits: [] } });
|
|
2028
|
+
}
|
|
2029
|
+
try {
|
|
2030
|
+
const hub = await (0, hub_1.hubSearchSkills)(this.store, this.ctx, { query, maxResults: Number(url.searchParams.get("maxResults") || 20) });
|
|
2031
|
+
this.jsonResponse(res, { local: { hits: localHits }, hub });
|
|
2032
|
+
}
|
|
2033
|
+
catch (err) {
|
|
2034
|
+
this.jsonResponse(res, { local: { hits: localHits }, hub: { hits: [] }, error: String(err) });
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
catch (err) {
|
|
2038
|
+
this.jsonResponse(res, { local: { hits: [] }, hub: { hits: [] }, error: String(err) });
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
searchLocalViewerMemories(query, options) {
|
|
2042
|
+
const db = this.store.db;
|
|
2043
|
+
const role = options?.role;
|
|
2044
|
+
const maxResults = options?.maxResults ?? 10;
|
|
2045
|
+
const params = [];
|
|
2046
|
+
let rows = [];
|
|
2047
|
+
try {
|
|
2048
|
+
let sql = "SELECT c.* FROM chunks_fts f JOIN chunks c ON f.rowid = c.rowid WHERE chunks_fts MATCH ?";
|
|
2049
|
+
params.push(query);
|
|
2050
|
+
if (role) {
|
|
2051
|
+
sql += " AND c.role = ?";
|
|
2052
|
+
params.push(role);
|
|
2053
|
+
}
|
|
2054
|
+
sql += " ORDER BY rank LIMIT ?";
|
|
2055
|
+
params.push(maxResults);
|
|
2056
|
+
rows = db.prepare(sql).all(...params);
|
|
2057
|
+
}
|
|
2058
|
+
catch {
|
|
2059
|
+
const likeParams = [`%${query}%`, `%${query}%`];
|
|
2060
|
+
let sql = "SELECT * FROM chunks WHERE (content LIKE ? OR summary LIKE ?)";
|
|
2061
|
+
if (role) {
|
|
2062
|
+
sql += " AND role = ?";
|
|
2063
|
+
likeParams.push(role);
|
|
2064
|
+
}
|
|
2065
|
+
sql += " ORDER BY created_at DESC LIMIT ?";
|
|
2066
|
+
likeParams.push(maxResults);
|
|
2067
|
+
rows = db.prepare(sql).all(...likeParams);
|
|
2068
|
+
}
|
|
2069
|
+
const hits = rows.map((row, idx) => ({
|
|
2070
|
+
id: row.id,
|
|
2071
|
+
summary: row.summary || row.content?.slice(0, 120) || "",
|
|
2072
|
+
excerpt: row.content || "",
|
|
2073
|
+
score: Math.max(0.3, 1 - idx * 0.1),
|
|
2074
|
+
role: row.role,
|
|
2075
|
+
ref: { sessionKey: row.session_key, chunkId: row.id, turnId: row.turn_id, seq: row.seq },
|
|
2076
|
+
taskId: row.task_id ?? null,
|
|
2077
|
+
skillId: row.skill_id ?? null,
|
|
2078
|
+
}));
|
|
2079
|
+
return { hits, meta: { total: hits.length, usedMaxResults: maxResults } };
|
|
2080
|
+
}
|
|
2081
|
+
handleSharingTaskShare(req, res) {
|
|
2082
|
+
this.readBody(req, async (body) => {
|
|
2083
|
+
if (!this.ctx)
|
|
2084
|
+
return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
2085
|
+
try {
|
|
2086
|
+
const parsed = JSON.parse(body || "{}");
|
|
2087
|
+
const taskId = String(parsed.taskId || "");
|
|
2088
|
+
const visibility = "public";
|
|
2089
|
+
const groupId = undefined;
|
|
2090
|
+
const task = this.store.getTask(taskId);
|
|
2091
|
+
if (!task)
|
|
2092
|
+
return this.jsonResponse(res, { ok: false, error: "task_not_found" });
|
|
2093
|
+
const chunks = this.store.getChunksByTask(taskId);
|
|
2094
|
+
if (chunks.length === 0)
|
|
2095
|
+
return this.jsonResponse(res, { ok: false, error: "no_chunks" });
|
|
2096
|
+
const hubClient = await this.resolveHubClientAware();
|
|
2097
|
+
const response = await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/share", {
|
|
2098
|
+
method: "POST",
|
|
2099
|
+
body: JSON.stringify({
|
|
2100
|
+
task: {
|
|
2101
|
+
id: task.id,
|
|
2102
|
+
sourceTaskId: task.id,
|
|
2103
|
+
title: task.title,
|
|
2104
|
+
summary: task.summary,
|
|
2105
|
+
groupId: null,
|
|
2106
|
+
visibility,
|
|
2107
|
+
createdAt: task.startedAt ?? Date.now(),
|
|
2108
|
+
updatedAt: task.updatedAt ?? Date.now(),
|
|
2109
|
+
},
|
|
2110
|
+
chunks: chunks.map((chunk) => ({
|
|
2111
|
+
id: chunk.id,
|
|
2112
|
+
hubTaskId: task.id,
|
|
2113
|
+
sourceTaskId: task.id,
|
|
2114
|
+
sourceChunkId: chunk.id,
|
|
2115
|
+
role: chunk.role,
|
|
2116
|
+
content: chunk.content,
|
|
2117
|
+
summary: chunk.summary,
|
|
2118
|
+
kind: chunk.kind,
|
|
2119
|
+
createdAt: chunk.createdAt,
|
|
2120
|
+
})),
|
|
2121
|
+
}),
|
|
2122
|
+
});
|
|
2123
|
+
const hubUserId = hubClient.userId;
|
|
2124
|
+
if (hubUserId) {
|
|
2125
|
+
this.store.upsertHubTask({
|
|
2126
|
+
id: task.id,
|
|
2127
|
+
sourceTaskId: task.id,
|
|
2128
|
+
sourceUserId: hubUserId,
|
|
2129
|
+
title: task.title,
|
|
2130
|
+
summary: task.summary,
|
|
2131
|
+
groupId: null,
|
|
2132
|
+
visibility,
|
|
2133
|
+
createdAt: task.startedAt ?? Date.now(),
|
|
2134
|
+
updatedAt: task.updatedAt ?? Date.now(),
|
|
2135
|
+
});
|
|
2136
|
+
}
|
|
2137
|
+
this.jsonResponse(res, { ok: true, taskId, visibility, response });
|
|
2138
|
+
}
|
|
2139
|
+
catch (err) {
|
|
2140
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
2141
|
+
}
|
|
2142
|
+
});
|
|
2143
|
+
}
|
|
2144
|
+
handleSharingTaskUnshare(req, res) {
|
|
2145
|
+
this.readBody(req, async (body) => {
|
|
2146
|
+
if (!this.ctx)
|
|
2147
|
+
return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
2148
|
+
try {
|
|
2149
|
+
const parsed = JSON.parse(body || "{}");
|
|
2150
|
+
const taskId = String(parsed.taskId || "");
|
|
2151
|
+
const task = this.store.getTask(taskId);
|
|
2152
|
+
if (!task)
|
|
2153
|
+
return this.jsonResponse(res, { ok: false, error: "task_not_found" });
|
|
2154
|
+
const hubClient = await this.resolveHubClientAware();
|
|
2155
|
+
await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/unshare", {
|
|
2156
|
+
method: "POST",
|
|
2157
|
+
body: JSON.stringify({ sourceTaskId: task.id }),
|
|
2158
|
+
});
|
|
2159
|
+
const hubUserId = hubClient.userId;
|
|
2160
|
+
if (hubUserId)
|
|
2161
|
+
this.store.deleteHubTaskBySource(hubUserId, task.id);
|
|
2162
|
+
this.jsonResponse(res, { ok: true, taskId });
|
|
2163
|
+
}
|
|
2164
|
+
catch (err) {
|
|
2165
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
2166
|
+
}
|
|
2167
|
+
});
|
|
2168
|
+
}
|
|
2169
|
+
handleSharingMemoryShare(req, res) {
|
|
2170
|
+
this.readBody(req, async (body) => {
|
|
2171
|
+
if (!this.ctx)
|
|
2172
|
+
return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
2173
|
+
try {
|
|
2174
|
+
const parsed = JSON.parse(body || "{}");
|
|
2175
|
+
const chunkId = String(parsed.chunkId || "");
|
|
2176
|
+
const visibility = "public";
|
|
2177
|
+
const groupId = undefined;
|
|
2178
|
+
const db = this.store.db;
|
|
2179
|
+
const chunk = db.prepare("SELECT * FROM chunks WHERE id = ?").get(chunkId);
|
|
2180
|
+
if (!chunk)
|
|
2181
|
+
return this.jsonResponse(res, { ok: false, error: "memory_not_found" });
|
|
2182
|
+
const hubClient = await this.resolveHubClientAware();
|
|
2183
|
+
const response = await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/share", {
|
|
2184
|
+
method: "POST",
|
|
2185
|
+
body: JSON.stringify({
|
|
2186
|
+
memory: {
|
|
2187
|
+
sourceChunkId: chunk.id,
|
|
2188
|
+
role: chunk.role,
|
|
2189
|
+
content: chunk.content,
|
|
2190
|
+
summary: chunk.summary,
|
|
2191
|
+
kind: chunk.kind,
|
|
2192
|
+
groupId: null,
|
|
2193
|
+
visibility,
|
|
2194
|
+
},
|
|
2195
|
+
}),
|
|
2196
|
+
});
|
|
2197
|
+
const hubUserId = hubClient.userId;
|
|
2198
|
+
if (hubUserId) {
|
|
2199
|
+
const now = Date.now();
|
|
2200
|
+
const existing = this.store.getHubMemoryBySource(hubUserId, chunk.id);
|
|
2201
|
+
this.store.upsertHubMemory({
|
|
2202
|
+
id: response?.memoryId ?? existing?.id ?? node_crypto_1.default.randomUUID(),
|
|
2203
|
+
sourceChunkId: chunk.id,
|
|
2204
|
+
sourceUserId: hubUserId,
|
|
2205
|
+
role: chunk.role,
|
|
2206
|
+
content: chunk.content,
|
|
2207
|
+
summary: chunk.summary ?? "",
|
|
2208
|
+
kind: chunk.kind,
|
|
2209
|
+
groupId: null,
|
|
2210
|
+
visibility,
|
|
2211
|
+
createdAt: existing?.createdAt ?? now,
|
|
2212
|
+
updatedAt: now,
|
|
2213
|
+
});
|
|
2214
|
+
}
|
|
2215
|
+
this.jsonResponse(res, { ok: true, chunkId, visibility, response });
|
|
2216
|
+
}
|
|
2217
|
+
catch (err) {
|
|
2218
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
2219
|
+
}
|
|
2220
|
+
});
|
|
2221
|
+
}
|
|
2222
|
+
handleSharingMemoryUnshare(req, res) {
|
|
2223
|
+
this.readBody(req, async (body) => {
|
|
2224
|
+
if (!this.ctx)
|
|
2225
|
+
return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
2226
|
+
try {
|
|
2227
|
+
const parsed = JSON.parse(body || "{}");
|
|
2228
|
+
const chunkId = String(parsed.chunkId || "");
|
|
2229
|
+
const hubClient = await this.resolveHubClientAware();
|
|
2230
|
+
await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/unshare", {
|
|
2231
|
+
method: "POST",
|
|
2232
|
+
body: JSON.stringify({ sourceChunkId: chunkId }),
|
|
2233
|
+
});
|
|
2234
|
+
const hubUserId = hubClient.userId;
|
|
2235
|
+
if (hubUserId)
|
|
2236
|
+
this.store.deleteHubMemoryBySource(hubUserId, chunkId);
|
|
2237
|
+
this.jsonResponse(res, { ok: true, chunkId });
|
|
2238
|
+
}
|
|
2239
|
+
catch (err) {
|
|
2240
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
2241
|
+
}
|
|
2242
|
+
});
|
|
2243
|
+
}
|
|
2244
|
+
handleSharingSkillPull(req, res) {
|
|
2245
|
+
this.readBody(req, async (body) => {
|
|
2246
|
+
if (!this.ctx)
|
|
2247
|
+
return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
2248
|
+
try {
|
|
2249
|
+
const parsed = JSON.parse(body || "{}");
|
|
2250
|
+
const skillId = String(parsed.skillId || "");
|
|
2251
|
+
const payload = await (0, skill_sync_1.fetchHubSkillBundle)(this.store, this.ctx, { skillId });
|
|
2252
|
+
const restored = (0, skill_sync_1.restoreSkillBundleFromHub)(this.store, this.ctx, payload);
|
|
2253
|
+
this.jsonResponse(res, { ok: true, pulled: true, hubSkillId: skillId, ...restored });
|
|
2254
|
+
}
|
|
2255
|
+
catch (err) {
|
|
2256
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
2257
|
+
}
|
|
2258
|
+
});
|
|
2259
|
+
}
|
|
2260
|
+
handleSharingSkillShare(req, res) {
|
|
2261
|
+
this.readBody(req, async (body) => {
|
|
2262
|
+
if (!this.ctx)
|
|
2263
|
+
return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
2264
|
+
try {
|
|
2265
|
+
const parsed = JSON.parse(body || "{}");
|
|
2266
|
+
const skillId = String(parsed.skillId || "");
|
|
2267
|
+
const visibility = "public";
|
|
2268
|
+
const groupId = null;
|
|
2269
|
+
const skill = this.store.getSkill(skillId);
|
|
2270
|
+
if (!skill)
|
|
2271
|
+
return this.jsonResponse(res, { ok: false, error: "skill_not_found" });
|
|
2272
|
+
const bundle = (0, skill_sync_1.buildSkillBundleForHub)(this.store, skillId);
|
|
2273
|
+
const hubClient = await this.resolveHubClientAware();
|
|
2274
|
+
const response = await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/publish", {
|
|
2275
|
+
method: "POST",
|
|
2276
|
+
body: JSON.stringify({
|
|
2277
|
+
visibility,
|
|
2278
|
+
groupId: null,
|
|
2279
|
+
metadata: bundle.metadata,
|
|
2280
|
+
bundle: bundle.bundle,
|
|
2281
|
+
}),
|
|
2282
|
+
});
|
|
2283
|
+
const hubUserId = hubClient.userId;
|
|
2284
|
+
if (hubUserId) {
|
|
2285
|
+
const existing = this.store.getHubSkillBySource(hubUserId, skillId);
|
|
2286
|
+
this.store.upsertHubSkill({
|
|
2287
|
+
id: response?.skillId ?? existing?.id ?? node_crypto_1.default.randomUUID(),
|
|
2288
|
+
sourceSkillId: skillId,
|
|
2289
|
+
sourceUserId: hubUserId,
|
|
2290
|
+
name: skill.name,
|
|
2291
|
+
description: skill.description,
|
|
2292
|
+
version: skill.version,
|
|
2293
|
+
groupId: null,
|
|
2294
|
+
visibility,
|
|
2295
|
+
bundle: JSON.stringify(bundle.bundle),
|
|
2296
|
+
qualityScore: skill.qualityScore,
|
|
2297
|
+
createdAt: existing?.createdAt ?? Date.now(),
|
|
2298
|
+
updatedAt: Date.now(),
|
|
2299
|
+
});
|
|
2300
|
+
}
|
|
2301
|
+
this.jsonResponse(res, { ok: true, skillId, visibility, response });
|
|
2302
|
+
}
|
|
2303
|
+
catch (err) {
|
|
2304
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
2305
|
+
}
|
|
2306
|
+
});
|
|
2307
|
+
}
|
|
2308
|
+
handleSharingSkillUnshare(req, res) {
|
|
2309
|
+
this.readBody(req, async (body) => {
|
|
2310
|
+
if (!this.ctx)
|
|
2311
|
+
return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
2312
|
+
try {
|
|
2313
|
+
const parsed = JSON.parse(body || "{}");
|
|
2314
|
+
const skillId = String(parsed.skillId || "");
|
|
2315
|
+
const skill = this.store.getSkill(skillId);
|
|
2316
|
+
if (!skill)
|
|
2317
|
+
return this.jsonResponse(res, { ok: false, error: "skill_not_found" });
|
|
2318
|
+
const hubClient = await this.resolveHubClientAware();
|
|
2319
|
+
await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/unpublish", {
|
|
2320
|
+
method: "POST",
|
|
2321
|
+
body: JSON.stringify({ sourceSkillId: skill.id }),
|
|
2322
|
+
});
|
|
2323
|
+
const hubUserId = hubClient.userId;
|
|
2324
|
+
if (hubUserId)
|
|
2325
|
+
this.store.deleteHubSkillBySource(hubUserId, skill.id);
|
|
2326
|
+
this.jsonResponse(res, { ok: true, skillId });
|
|
2327
|
+
}
|
|
2328
|
+
catch (err) {
|
|
2329
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
2330
|
+
}
|
|
2331
|
+
});
|
|
2332
|
+
}
|
|
2333
|
+
resolveHubConnection() {
|
|
2334
|
+
if (!this.ctx)
|
|
2335
|
+
return null;
|
|
2336
|
+
// Hub 模式:连接自己,用 bootstrap admin token
|
|
2337
|
+
const sharing = this.ctx.config.sharing;
|
|
2338
|
+
if (sharing?.role === "hub") {
|
|
2339
|
+
const hubPort = sharing.hub?.port ?? 18800;
|
|
2340
|
+
const hubUrl = `http://127.0.0.1:${hubPort}`;
|
|
2341
|
+
try {
|
|
2342
|
+
const authPath = node_path_1.default.join(this.dataDir, "hub-auth.json");
|
|
2343
|
+
const authData = JSON.parse(node_fs_1.default.readFileSync(authPath, "utf8"));
|
|
2344
|
+
const adminToken = authData?.bootstrapAdminToken;
|
|
2345
|
+
if (adminToken)
|
|
2346
|
+
return { hubUrl, userToken: adminToken };
|
|
2347
|
+
}
|
|
2348
|
+
catch {
|
|
2349
|
+
// hub-auth.json 不存在或读取失败,fall through
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
// Client 模式:用配置的 hubAddress + userToken
|
|
2353
|
+
const conn = this.store.getClientHubConnection();
|
|
2354
|
+
const hubUrl = conn?.hubUrl || this.ctx.config.sharing?.client?.hubAddress || "";
|
|
2355
|
+
const userToken = conn?.userToken || this.ctx.config.sharing?.client?.userToken || "";
|
|
2356
|
+
if (!hubUrl || !userToken)
|
|
2357
|
+
return null;
|
|
2358
|
+
return { hubUrl: (0, hub_1.normalizeHubUrl)(hubUrl), userToken };
|
|
2359
|
+
}
|
|
2360
|
+
/** resolveHubClient 的 viewer 版本:hub 模式下使用 bootstrap admin 身份 */
|
|
2361
|
+
async resolveHubClientAware() {
|
|
2362
|
+
if (!this.ctx)
|
|
2363
|
+
throw new Error("sharing_unavailable");
|
|
2364
|
+
const sharing = this.ctx.config.sharing;
|
|
2365
|
+
if (sharing?.role === "hub") {
|
|
2366
|
+
const hub = this.resolveHubConnection();
|
|
2367
|
+
if (hub) {
|
|
2368
|
+
const me = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/me", { method: "GET" });
|
|
2369
|
+
return {
|
|
2370
|
+
hubUrl: hub.hubUrl,
|
|
2371
|
+
userToken: hub.userToken,
|
|
2372
|
+
userId: String(me.id),
|
|
2373
|
+
username: String(me.username ?? "hub-admin"),
|
|
2374
|
+
role: String(me.role ?? "admin"),
|
|
2375
|
+
};
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
return (0, hub_1.resolveHubClient)(this.store, this.ctx);
|
|
2379
|
+
}
|
|
2380
|
+
async serveSharingUsers(res) {
|
|
2381
|
+
const hub = this.resolveHubConnection();
|
|
2382
|
+
if (!hub)
|
|
2383
|
+
return this.jsonResponse(res, { users: [], error: "not_configured" });
|
|
2384
|
+
try {
|
|
2385
|
+
const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/users", { method: "GET" });
|
|
2386
|
+
this.jsonResponse(res, { users: Array.isArray(data?.users) ? data.users : [] });
|
|
2387
|
+
}
|
|
2388
|
+
catch (err) {
|
|
2389
|
+
this.jsonResponse(res, { users: [], error: String(err) });
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
// ─── Admin management endpoints (Hub-side data) ───
|
|
2393
|
+
async serveAdminSharedTasks(res) {
|
|
2394
|
+
const hub = this.resolveHubConnection();
|
|
2395
|
+
if (!hub)
|
|
2396
|
+
return this.jsonResponse(res, { tasks: [], error: "not_configured" });
|
|
2397
|
+
try {
|
|
2398
|
+
const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/shared-tasks", { method: "GET" });
|
|
2399
|
+
const tasks = Array.isArray(data?.tasks) ? data.tasks : [];
|
|
2400
|
+
for (const tk of tasks) {
|
|
2401
|
+
if (!tk.summary && tk.sourceTaskId) {
|
|
2402
|
+
const local = this.store.getTask(tk.sourceTaskId);
|
|
2403
|
+
if (local) {
|
|
2404
|
+
tk.summary = local.summary;
|
|
2405
|
+
tk.title = tk.title || local.title;
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
this.jsonResponse(res, { tasks });
|
|
2410
|
+
}
|
|
2411
|
+
catch (err) {
|
|
2412
|
+
this.jsonResponse(res, { tasks: [], error: String(err) });
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
async handleAdminDeleteTask(res, p) {
|
|
2416
|
+
const hub = this.resolveHubConnection();
|
|
2417
|
+
if (!hub)
|
|
2418
|
+
return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
2419
|
+
const taskId = decodeURIComponent(p.replace("/api/admin/shared-tasks/", ""));
|
|
2420
|
+
try {
|
|
2421
|
+
await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/admin/shared-tasks/${encodeURIComponent(taskId)}`, { method: "DELETE" });
|
|
2422
|
+
this.jsonResponse(res, { ok: true });
|
|
2423
|
+
}
|
|
2424
|
+
catch (err) {
|
|
2425
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
async serveHubTaskDetail(res, p) {
|
|
2429
|
+
const hub = this.resolveHubConnection();
|
|
2430
|
+
if (!hub)
|
|
2431
|
+
return this.jsonResponse(res, { error: "not_configured" }, 500);
|
|
2432
|
+
const m = p.match(/^\/api\/admin\/shared-tasks\/([^/]+)\/detail$/);
|
|
2433
|
+
if (!m)
|
|
2434
|
+
return this.jsonResponse(res, { error: "bad_request" }, 400);
|
|
2435
|
+
const taskId = decodeURIComponent(m[1]);
|
|
2436
|
+
try {
|
|
2437
|
+
const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/shared-tasks/${encodeURIComponent(taskId)}/detail`, { method: "GET" });
|
|
2438
|
+
this.jsonResponse(res, data);
|
|
2439
|
+
}
|
|
2440
|
+
catch (err) {
|
|
2441
|
+
this.jsonResponse(res, { error: String(err) }, 500);
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
async serveHubSkillDetail(res, p) {
|
|
2445
|
+
const hub = this.resolveHubConnection();
|
|
2446
|
+
if (!hub)
|
|
2447
|
+
return this.jsonResponse(res, { error: "not_configured" }, 500);
|
|
2448
|
+
const m = p.match(/^\/api\/admin\/shared-skills\/([^/]+)\/detail$/);
|
|
2449
|
+
if (!m)
|
|
2450
|
+
return this.jsonResponse(res, { error: "bad_request" }, 400);
|
|
2451
|
+
const skillId = decodeURIComponent(m[1]);
|
|
2452
|
+
try {
|
|
2453
|
+
const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/shared-skills/${encodeURIComponent(skillId)}/detail`, { method: "GET" });
|
|
2454
|
+
this.jsonResponse(res, data);
|
|
2455
|
+
}
|
|
2456
|
+
catch (err) {
|
|
2457
|
+
this.jsonResponse(res, { error: String(err) }, 500);
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
async serveAdminSharedSkills(res) {
|
|
2461
|
+
const hub = this.resolveHubConnection();
|
|
2462
|
+
if (!hub)
|
|
2463
|
+
return this.jsonResponse(res, { skills: [], error: "not_configured" });
|
|
2464
|
+
try {
|
|
2465
|
+
const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/shared-skills", { method: "GET" });
|
|
2466
|
+
const skills = Array.isArray(data?.skills) ? data.skills : [];
|
|
2467
|
+
for (const sk of skills) {
|
|
2468
|
+
if (!sk.description && sk.sourceSkillId) {
|
|
2469
|
+
const local = this.store.getSkill(sk.sourceSkillId);
|
|
2470
|
+
if (local) {
|
|
2471
|
+
sk.description = sk.description || local.description;
|
|
2472
|
+
sk.name = sk.name || local.name;
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
this.jsonResponse(res, { skills });
|
|
2477
|
+
}
|
|
2478
|
+
catch (err) {
|
|
2479
|
+
this.jsonResponse(res, { skills: [], error: String(err) });
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
async handleAdminDeleteSkill(res, p) {
|
|
2483
|
+
const hub = this.resolveHubConnection();
|
|
2484
|
+
if (!hub)
|
|
2485
|
+
return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
2486
|
+
const skillId = decodeURIComponent(p.replace("/api/admin/shared-skills/", ""));
|
|
2487
|
+
try {
|
|
2488
|
+
await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/admin/shared-skills/${encodeURIComponent(skillId)}`, { method: "DELETE" });
|
|
2489
|
+
this.jsonResponse(res, { ok: true });
|
|
2490
|
+
}
|
|
2491
|
+
catch (err) {
|
|
2492
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
async serveAdminSharedMemories(res) {
|
|
2496
|
+
const hub = this.resolveHubConnection();
|
|
2497
|
+
if (!hub)
|
|
2498
|
+
return this.jsonResponse(res, { memories: [], error: "not_configured" });
|
|
2499
|
+
try {
|
|
2500
|
+
const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/shared-memories", { method: "GET" });
|
|
2501
|
+
const memories = Array.isArray(data?.memories) ? data.memories : [];
|
|
2502
|
+
for (const m of memories) {
|
|
2503
|
+
if (!m.content && m.sourceChunkId) {
|
|
2504
|
+
const local = this.store.getChunk(m.sourceChunkId);
|
|
2505
|
+
if (local) {
|
|
2506
|
+
m.content = local.content;
|
|
2507
|
+
if (!m.summary && local.summary)
|
|
2508
|
+
m.summary = local.summary;
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
this.jsonResponse(res, { memories });
|
|
2513
|
+
}
|
|
2514
|
+
catch (err) {
|
|
2515
|
+
this.jsonResponse(res, { memories: [], error: String(err) });
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
async handleAdminDeleteMemory(res, p) {
|
|
2519
|
+
const hub = this.resolveHubConnection();
|
|
2520
|
+
if (!hub)
|
|
2521
|
+
return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
2522
|
+
const memoryId = decodeURIComponent(p.replace("/api/admin/shared-memories/", ""));
|
|
2523
|
+
try {
|
|
2524
|
+
await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/admin/shared-memories/${encodeURIComponent(memoryId)}`, { method: "DELETE" });
|
|
2525
|
+
this.jsonResponse(res, { ok: true });
|
|
2526
|
+
}
|
|
2527
|
+
catch (err) {
|
|
2528
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
async serveSharingNotifications(res, url) {
|
|
2532
|
+
const hub = this.resolveHubConnection();
|
|
2533
|
+
if (!hub)
|
|
2534
|
+
return this.jsonResponse(res, { notifications: [], unreadCount: 0 });
|
|
2535
|
+
try {
|
|
2536
|
+
const unread = url.searchParams.get("unread") === "1" ? "?unread=1" : "";
|
|
2537
|
+
const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/notifications${unread}`);
|
|
2538
|
+
this.jsonResponse(res, data);
|
|
2539
|
+
}
|
|
2540
|
+
catch {
|
|
2541
|
+
this.jsonResponse(res, { notifications: [], unreadCount: 0 });
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
handleSharingNotificationsRead(req, res) {
|
|
2545
|
+
const hub = this.resolveHubConnection();
|
|
2546
|
+
if (!hub)
|
|
2547
|
+
return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
2548
|
+
this.readBody(req, async (raw) => {
|
|
2549
|
+
try {
|
|
2550
|
+
const body = JSON.parse(raw || "{}");
|
|
2551
|
+
await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications/read", { method: "POST", body: JSON.stringify(body) });
|
|
2552
|
+
this.jsonResponse(res, { ok: true });
|
|
2553
|
+
try {
|
|
2554
|
+
const data = (await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications?unread=1"));
|
|
2555
|
+
const count = data?.unreadCount ?? 0;
|
|
2556
|
+
this.lastKnownNotifCount = count;
|
|
2557
|
+
this.broadcastNotifSSE({ type: "update", unreadCount: count });
|
|
2558
|
+
}
|
|
2559
|
+
catch { /* best effort */ }
|
|
2560
|
+
}
|
|
2561
|
+
catch (err) {
|
|
2562
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
2563
|
+
}
|
|
2564
|
+
});
|
|
2565
|
+
}
|
|
2566
|
+
handleSharingNotificationsClear(req, res) {
|
|
2567
|
+
const hub = this.resolveHubConnection();
|
|
2568
|
+
if (!hub)
|
|
2569
|
+
return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
2570
|
+
this.readBody(req, async () => {
|
|
2571
|
+
try {
|
|
2572
|
+
await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications/clear", { method: "POST", body: "{}" });
|
|
2573
|
+
this.jsonResponse(res, { ok: true });
|
|
2574
|
+
this.broadcastNotifSSE({ type: "cleared", unreadCount: 0 });
|
|
2575
|
+
}
|
|
2576
|
+
catch (err) {
|
|
2577
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
2578
|
+
}
|
|
2579
|
+
});
|
|
2580
|
+
}
|
|
2581
|
+
handleNotifSSE(req, res) {
|
|
2582
|
+
res.writeHead(200, {
|
|
2583
|
+
"Content-Type": "text/event-stream",
|
|
2584
|
+
"Cache-Control": "no-cache",
|
|
2585
|
+
Connection: "keep-alive",
|
|
2586
|
+
"Access-Control-Allow-Origin": "*",
|
|
2587
|
+
});
|
|
2588
|
+
res.write("data: {\"type\":\"connected\"}\n\n");
|
|
2589
|
+
this.notifSSEClients.push(res);
|
|
2590
|
+
if (!this.notifPollTimer)
|
|
2591
|
+
this.startNotifPoll();
|
|
2592
|
+
req.on("close", () => {
|
|
2593
|
+
this.notifSSEClients = this.notifSSEClients.filter((c) => c !== res);
|
|
2594
|
+
if (this.notifSSEClients.length === 0)
|
|
2595
|
+
this.stopNotifPoll();
|
|
2596
|
+
});
|
|
2597
|
+
}
|
|
2598
|
+
broadcastNotifSSE(data) {
|
|
2599
|
+
const msg = `data: ${JSON.stringify(data)}\n\n`;
|
|
2600
|
+
this.notifSSEClients = this.notifSSEClients.filter((c) => {
|
|
2601
|
+
try {
|
|
2602
|
+
c.write(msg);
|
|
2603
|
+
return true;
|
|
2604
|
+
}
|
|
2605
|
+
catch {
|
|
2606
|
+
return false;
|
|
2607
|
+
}
|
|
2608
|
+
});
|
|
2609
|
+
}
|
|
2610
|
+
startNotifPoll() {
|
|
2611
|
+
this.stopNotifPoll();
|
|
2612
|
+
const tick = async () => {
|
|
2613
|
+
const hub = this.resolveHubConnection();
|
|
2614
|
+
if (!hub)
|
|
2615
|
+
return;
|
|
2616
|
+
try {
|
|
2617
|
+
const data = (await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications?unread=1"));
|
|
2618
|
+
const count = data?.unreadCount ?? 0;
|
|
2619
|
+
if (count !== this.lastKnownNotifCount) {
|
|
2620
|
+
this.lastKnownNotifCount = count;
|
|
2621
|
+
this.broadcastNotifSSE({ type: "update", unreadCount: count });
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
catch { /* ignore */ }
|
|
2625
|
+
};
|
|
2626
|
+
tick();
|
|
2627
|
+
this.notifPollTimer = setInterval(tick, 3000);
|
|
2628
|
+
}
|
|
2629
|
+
stopNotifPoll() {
|
|
2630
|
+
if (this.notifPollTimer) {
|
|
2631
|
+
clearInterval(this.notifPollTimer);
|
|
2632
|
+
this.notifPollTimer = undefined;
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
startHubHeartbeat() {
|
|
2636
|
+
this.stopHubHeartbeat();
|
|
2637
|
+
const sendHeartbeat = async () => {
|
|
2638
|
+
try {
|
|
2639
|
+
const hub = this.resolveHubConnection();
|
|
2640
|
+
if (!hub) {
|
|
2641
|
+
const persisted = this.store.getClientHubConnection();
|
|
2642
|
+
if (persisted?.hubUrl && persisted?.userToken) {
|
|
2643
|
+
await (0, hub_1.hubRequestJson)(persisted.hubUrl, persisted.userToken, "/api/v1/hub/heartbeat", { method: "POST" });
|
|
2644
|
+
}
|
|
2645
|
+
return;
|
|
2646
|
+
}
|
|
2647
|
+
await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/heartbeat", { method: "POST" });
|
|
2648
|
+
}
|
|
2649
|
+
catch { /* best-effort */ }
|
|
2650
|
+
};
|
|
2651
|
+
sendHeartbeat();
|
|
2652
|
+
this.hubHeartbeatTimer = setInterval(sendHeartbeat, ViewerServer.HUB_HEARTBEAT_INTERVAL_MS);
|
|
2653
|
+
}
|
|
2654
|
+
stopHubHeartbeat() {
|
|
2655
|
+
if (this.hubHeartbeatTimer) {
|
|
2656
|
+
clearInterval(this.hubHeartbeatTimer);
|
|
2657
|
+
this.hubHeartbeatTimer = undefined;
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
getLocalIPs() {
|
|
2661
|
+
const nets = node_os_1.default.networkInterfaces();
|
|
2662
|
+
const ips = [];
|
|
2663
|
+
for (const name of Object.keys(nets)) {
|
|
2664
|
+
for (const net of nets[name] ?? []) {
|
|
2665
|
+
if (net.family === "IPv4" && !net.internal) {
|
|
2666
|
+
ips.push(net.address);
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
return ips;
|
|
2671
|
+
}
|
|
2672
|
+
serveLocalIPs(res) {
|
|
2673
|
+
const ips = this.getLocalIPs();
|
|
2674
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2675
|
+
res.end(JSON.stringify({ ips }));
|
|
2676
|
+
}
|
|
2677
|
+
serveConfig(res) {
|
|
2678
|
+
try {
|
|
2679
|
+
const cfgPath = this.getOpenClawConfigPath();
|
|
2680
|
+
if (!node_fs_1.default.existsSync(cfgPath)) {
|
|
2681
|
+
this.jsonResponse(res, {});
|
|
2682
|
+
return;
|
|
2683
|
+
}
|
|
2684
|
+
const raw = JSON.parse(node_fs_1.default.readFileSync(cfgPath, "utf-8"));
|
|
2685
|
+
const entries = raw?.plugins?.entries ?? {};
|
|
2686
|
+
const pluginEntry = entries["memos-local-openclaw-plugin"]?.config
|
|
2687
|
+
?? entries["memos-local"]?.config
|
|
2688
|
+
?? entries["memos-lite-openclaw-plugin"]?.config
|
|
2689
|
+
?? entries["memos-lite"]?.config
|
|
2690
|
+
?? {};
|
|
2691
|
+
const result = { ...pluginEntry };
|
|
2692
|
+
const topEntry = entries["memos-local-openclaw-plugin"]
|
|
2693
|
+
?? entries["memos-local"]
|
|
2694
|
+
?? entries["memos-lite-openclaw-plugin"]
|
|
2695
|
+
?? entries["memos-lite"]
|
|
2696
|
+
?? {};
|
|
2697
|
+
if (pluginEntry.viewerPort != null) {
|
|
2698
|
+
result.viewerPort = pluginEntry.viewerPort;
|
|
2699
|
+
}
|
|
2700
|
+
else if (topEntry.viewerPort) {
|
|
2701
|
+
result.viewerPort = topEntry.viewerPort;
|
|
2702
|
+
}
|
|
2703
|
+
this.jsonResponse(res, result);
|
|
2704
|
+
}
|
|
2705
|
+
catch (e) {
|
|
2706
|
+
this.log.warn(`serveConfig error: ${e}`);
|
|
2707
|
+
this.jsonResponse(res, {});
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
handleSaveConfig(req, res) {
|
|
2711
|
+
this.readBody(req, async (body) => {
|
|
2712
|
+
try {
|
|
2713
|
+
const newCfg = JSON.parse(body);
|
|
2714
|
+
const cfgPath = this.getOpenClawConfigPath();
|
|
2715
|
+
let raw = {};
|
|
2716
|
+
if (node_fs_1.default.existsSync(cfgPath)) {
|
|
2717
|
+
raw = JSON.parse(node_fs_1.default.readFileSync(cfgPath, "utf-8"));
|
|
2718
|
+
}
|
|
2719
|
+
if (!raw.plugins)
|
|
2720
|
+
raw.plugins = {};
|
|
2721
|
+
const plugins = raw.plugins;
|
|
2722
|
+
if (!plugins.entries)
|
|
2723
|
+
plugins.entries = {};
|
|
2724
|
+
const entries = plugins.entries;
|
|
2725
|
+
const entryKey = entries["memos-local-openclaw-plugin"] ? "memos-local-openclaw-plugin"
|
|
2726
|
+
: entries["memos-local"] ? "memos-local"
|
|
2727
|
+
: entries["memos-lite-openclaw-plugin"] ? "memos-lite-openclaw-plugin"
|
|
2728
|
+
: entries["memos-lite"] ? "memos-lite"
|
|
2729
|
+
: "memos-local-openclaw-plugin";
|
|
2730
|
+
if (!entries[entryKey])
|
|
2731
|
+
entries[entryKey] = { enabled: true };
|
|
2732
|
+
const entry = entries[entryKey];
|
|
2733
|
+
if (!entry.config)
|
|
2734
|
+
entry.config = {};
|
|
2735
|
+
const config = entry.config;
|
|
2736
|
+
const oldSharingRole = config.sharing?.role;
|
|
2737
|
+
if (newCfg.embedding)
|
|
2738
|
+
config.embedding = newCfg.embedding;
|
|
2739
|
+
if (newCfg.summarizer)
|
|
2740
|
+
config.summarizer = newCfg.summarizer;
|
|
2741
|
+
if (newCfg.skillEvolution)
|
|
2742
|
+
config.skillEvolution = newCfg.skillEvolution;
|
|
2743
|
+
if (newCfg.viewerPort)
|
|
2744
|
+
config.viewerPort = newCfg.viewerPort;
|
|
2745
|
+
if (newCfg.telemetry !== undefined)
|
|
2746
|
+
config.telemetry = newCfg.telemetry;
|
|
2747
|
+
if (newCfg.sharing !== undefined) {
|
|
2748
|
+
const existing = config.sharing || {};
|
|
2749
|
+
const merged = { ...existing, ...newCfg.sharing };
|
|
2750
|
+
if (newCfg.sharing.capabilities && existing.capabilities) {
|
|
2751
|
+
merged.capabilities = { ...existing.capabilities, ...newCfg.sharing.capabilities };
|
|
2752
|
+
}
|
|
2753
|
+
if (merged.role === "client" && merged.client) {
|
|
2754
|
+
const clientCfg = merged.client;
|
|
2755
|
+
const addr = String(clientCfg.hubAddress || "");
|
|
2756
|
+
if (addr) {
|
|
2757
|
+
const localIPs = this.getLocalIPs();
|
|
2758
|
+
localIPs.push("127.0.0.1", "localhost", "0.0.0.0");
|
|
2759
|
+
try {
|
|
2760
|
+
const u = new URL(addr.startsWith("http") ? addr : `http://${addr}`);
|
|
2761
|
+
if (localIPs.includes(u.hostname)) {
|
|
2762
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2763
|
+
res.end(JSON.stringify({ error: "cannot_join_self" }));
|
|
2764
|
+
return;
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
catch { }
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
// When switching away from client mode, notify Hub that we're leaving
|
|
2771
|
+
const newRole = merged.role;
|
|
2772
|
+
if (oldSharingRole === "client" && newRole !== "client") {
|
|
2773
|
+
this.notifyHubLeave();
|
|
2774
|
+
}
|
|
2775
|
+
if (merged.role === "hub") {
|
|
2776
|
+
merged.client = { hubAddress: "", userToken: "", teamToken: "" };
|
|
2777
|
+
}
|
|
2778
|
+
else if (merged.role === "client") {
|
|
2779
|
+
merged.hub = { port: 18800, teamName: "", teamToken: "" };
|
|
2780
|
+
}
|
|
2781
|
+
config.sharing = merged;
|
|
2782
|
+
}
|
|
2783
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(cfgPath), { recursive: true });
|
|
2784
|
+
node_fs_1.default.writeFileSync(cfgPath, JSON.stringify(raw, null, 2), "utf-8");
|
|
2785
|
+
this.log.info("Plugin config updated via Viewer");
|
|
2786
|
+
this.stopHubHeartbeat();
|
|
2787
|
+
this.jsonResponse(res, { ok: true });
|
|
2788
|
+
}
|
|
2789
|
+
catch (e) {
|
|
2790
|
+
this.log.warn(`handleSaveConfig error: ${e}`);
|
|
2791
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
2792
|
+
res.end(JSON.stringify({ error: String(e) }));
|
|
2793
|
+
}
|
|
2794
|
+
});
|
|
2795
|
+
}
|
|
2796
|
+
async notifyHubLeave() {
|
|
2797
|
+
try {
|
|
2798
|
+
const hub = this.resolveHubConnection();
|
|
2799
|
+
if (hub) {
|
|
2800
|
+
await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/leave", { method: "POST" });
|
|
2801
|
+
this.log.info("Notified Hub of voluntary leave");
|
|
2802
|
+
return;
|
|
2803
|
+
}
|
|
2804
|
+
const persisted = this.store.getClientHubConnection();
|
|
2805
|
+
if (persisted?.hubUrl && persisted?.userToken) {
|
|
2806
|
+
await (0, hub_1.hubRequestJson)(persisted.hubUrl, persisted.userToken, "/api/v1/hub/leave", { method: "POST" });
|
|
2807
|
+
this.log.info("Notified Hub of voluntary leave (persisted connection)");
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
catch (e) {
|
|
2811
|
+
this.log.warn(`Failed to notify Hub of leave: ${e}`);
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
handleUpdateUsername(req, res) {
|
|
2815
|
+
this.readBody(req, async (body) => {
|
|
2816
|
+
if (!this.ctx)
|
|
2817
|
+
return this.jsonResponse(res, { error: "sharing_unavailable" });
|
|
2818
|
+
try {
|
|
2819
|
+
const { username } = JSON.parse(body || "{}");
|
|
2820
|
+
if (!username || typeof username !== "string" || username.trim().length < 2 || username.trim().length > 32) {
|
|
2821
|
+
return this.jsonResponse(res, { error: "invalid_username" }, 400);
|
|
2822
|
+
}
|
|
2823
|
+
const trimmed = username.trim();
|
|
2824
|
+
const hubClient = await this.resolveHubClientAware();
|
|
2825
|
+
const result = await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/me/update-profile", {
|
|
2826
|
+
method: "POST",
|
|
2827
|
+
body: JSON.stringify({ username: trimmed }),
|
|
2828
|
+
});
|
|
2829
|
+
if (result.ok && result.userToken) {
|
|
2830
|
+
const sharing = this.ctx.config.sharing;
|
|
2831
|
+
if (sharing?.role === "hub") {
|
|
2832
|
+
try {
|
|
2833
|
+
const authPath = node_path_1.default.join(this.dataDir, "hub-auth.json");
|
|
2834
|
+
const authData = JSON.parse(node_fs_1.default.readFileSync(authPath, "utf8"));
|
|
2835
|
+
authData.bootstrapAdminToken = result.userToken;
|
|
2836
|
+
node_fs_1.default.writeFileSync(authPath, JSON.stringify(authData, null, 2), "utf-8");
|
|
2837
|
+
this.log.info("hub-auth.json updated with new admin token after username change");
|
|
2838
|
+
}
|
|
2839
|
+
catch (e) {
|
|
2840
|
+
this.log.warn(`Failed to update hub-auth.json: ${e}`);
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
else {
|
|
2844
|
+
const persisted = this.store.getClientHubConnection();
|
|
2845
|
+
if (persisted) {
|
|
2846
|
+
this.store.setClientHubConnection({
|
|
2847
|
+
...persisted,
|
|
2848
|
+
username: result.username,
|
|
2849
|
+
userToken: result.userToken,
|
|
2850
|
+
});
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
this.jsonResponse(res, result);
|
|
2855
|
+
}
|
|
2856
|
+
catch (err) {
|
|
2857
|
+
const msg = String(err?.message || err);
|
|
2858
|
+
if (msg.includes("409") || msg.includes("username_taken")) {
|
|
2859
|
+
return this.jsonResponse(res, { error: "username_taken" }, 409);
|
|
2860
|
+
}
|
|
2861
|
+
this.jsonResponse(res, { error: msg }, 500);
|
|
2862
|
+
}
|
|
2863
|
+
});
|
|
2864
|
+
}
|
|
2865
|
+
handleTestHubConnection(req, res) {
|
|
2866
|
+
this.readBody(req, async (body) => {
|
|
2867
|
+
try {
|
|
2868
|
+
const { hubUrl } = JSON.parse(body);
|
|
2869
|
+
if (!hubUrl) {
|
|
2870
|
+
this.jsonResponse(res, { ok: false, error: "hubUrl is required" });
|
|
2871
|
+
return;
|
|
2872
|
+
}
|
|
2873
|
+
try {
|
|
2874
|
+
const localIPs = this.getLocalIPs();
|
|
2875
|
+
localIPs.push("127.0.0.1", "localhost", "0.0.0.0");
|
|
2876
|
+
const parsed = new URL(hubUrl.startsWith("http") ? hubUrl : `http://${hubUrl}`);
|
|
2877
|
+
if (localIPs.includes(parsed.hostname)) {
|
|
2878
|
+
this.jsonResponse(res, { ok: false, error: "cannot_join_self" });
|
|
2879
|
+
return;
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
catch { }
|
|
2883
|
+
const url = hubUrl.replace(/\/+$/, "") + "/api/v1/hub/info";
|
|
2884
|
+
const ctrl = new AbortController();
|
|
2885
|
+
const timeout = setTimeout(() => ctrl.abort(), 8000);
|
|
2886
|
+
try {
|
|
2887
|
+
const r = await fetch(url, { signal: ctrl.signal });
|
|
2888
|
+
clearTimeout(timeout);
|
|
2889
|
+
if (!r.ok) {
|
|
2890
|
+
this.jsonResponse(res, { ok: false, error: `HTTP ${r.status}` });
|
|
2891
|
+
return;
|
|
2892
|
+
}
|
|
2893
|
+
const info = await r.json();
|
|
2894
|
+
this.jsonResponse(res, { ok: true, teamName: info.teamName || "", apiVersion: info.apiVersion || "" });
|
|
2895
|
+
}
|
|
2896
|
+
catch (e) {
|
|
2897
|
+
clearTimeout(timeout);
|
|
2898
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2899
|
+
this.jsonResponse(res, { ok: false, error: msg.includes("abort") ? "Connection timeout (8s)" : msg });
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
catch (e) {
|
|
2903
|
+
this.jsonResponse(res, { ok: false, error: String(e) });
|
|
1150
2904
|
}
|
|
1151
2905
|
});
|
|
1152
2906
|
}
|
|
@@ -2461,8 +4215,8 @@ class ViewerServer {
|
|
|
2461
4215
|
req.on("data", (chunk) => { body += chunk.toString(); });
|
|
2462
4216
|
req.on("end", () => cb(body));
|
|
2463
4217
|
}
|
|
2464
|
-
jsonResponse(res, data) {
|
|
2465
|
-
res.writeHead(
|
|
4218
|
+
jsonResponse(res, data, statusCode = 200) {
|
|
4219
|
+
res.writeHead(statusCode, { "Content-Type": "application/json; charset=utf-8" });
|
|
2466
4220
|
res.end(JSON.stringify(data));
|
|
2467
4221
|
}
|
|
2468
4222
|
}
|