@memtensor/memos-local-openclaw-plugin 1.0.2 → 1.0.4-beta.0
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/client/connector.d.ts +26 -0
- package/dist/client/connector.d.ts.map +1 -0
- package/dist/client/connector.js +127 -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 +148 -0
- package/dist/client/hub.js.map +1 -0
- package/dist/client/skill-sync.d.ts +29 -0
- package/dist/client/skill-sync.d.ts.map +1 -0
- package/dist/client/skill-sync.js +216 -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 +21 -4
- 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 +41 -0
- package/dist/hub/server.d.ts.map +1 -0
- package/dist/hub/server.js +742 -0
- package/dist/hub/server.js.map +1 -0
- package/dist/hub/user-manager.d.ts +28 -0
- package/dist/hub/user-manager.d.ts.map +1 -0
- package/dist/hub/user-manager.js +112 -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 +4 -3
- 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 +242 -8
- 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 +2 -2
- 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 +15 -0
- 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/sqlite.d.ts +294 -0
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +902 -8
- 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 +3 -2
- package/dist/tools/memory-search.d.ts.map +1 -1
- package/dist/tools/memory-search.js +48 -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 +47 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/update-check.d.ts.map +1 -1
- package/dist/update-check.js +0 -1
- package/dist/update-check.js.map +1 -1
- package/dist/viewer/html.d.ts.map +1 -1
- package/dist/viewer/html.js +2396 -289
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +43 -0
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +1180 -33
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +445 -25
- package/openclaw.plugin.json +2 -1
- package/package.json +2 -1
- package/scripts/postinstall.cjs +282 -45
- package/skill/memos-memory-guide/SKILL.md +26 -2
- package/src/client/connector.ts +124 -0
- package/src/client/hub.ts +189 -0
- package/src/client/skill-sync.ts +202 -0
- package/src/config.ts +92 -3
- package/src/embedding/index.ts +25 -3
- package/src/hub/auth.ts +78 -0
- package/src/hub/server.ts +734 -0
- package/src/hub/user-manager.ts +126 -0
- package/src/index.ts +7 -4
- package/src/ingest/providers/index.ts +279 -8
- 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 +2 -2
- package/src/shared/llm-call.ts +19 -1
- 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/sqlite.ts +1167 -7
- package/src/tools/index.ts +1 -0
- package/src/tools/memory-search.ts +57 -8
- package/src/tools/network-memory-detail.ts +34 -0
- package/src/types.ts +48 -2
- package/src/update-check.ts +0 -1
- package/src/viewer/html.ts +2396 -289
- package/src/viewer/server.ts +1087 -34
package/dist/viewer/server.js
CHANGED
|
@@ -38,6 +38,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.ViewerServer = void 0;
|
|
40
40
|
const node_http_1 = __importDefault(require("node:http"));
|
|
41
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
41
42
|
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
42
43
|
const node_child_process_1 = require("node:child_process");
|
|
43
44
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
@@ -50,6 +51,10 @@ const vector_1 = require("../storage/vector");
|
|
|
50
51
|
const task_processor_1 = require("../ingest/task-processor");
|
|
51
52
|
const engine_1 = require("../recall/engine");
|
|
52
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");
|
|
53
58
|
const html_1 = require("./html");
|
|
54
59
|
const uuid_1 = require("uuid");
|
|
55
60
|
function normalizeTimestamp(ts) {
|
|
@@ -224,7 +229,7 @@ class ViewerServer {
|
|
|
224
229
|
if (p === "/api/memories" && req.method === "GET")
|
|
225
230
|
this.serveMemories(res, url);
|
|
226
231
|
else if (p === "/api/stats")
|
|
227
|
-
this.serveStats(res);
|
|
232
|
+
this.serveStats(res, url);
|
|
228
233
|
else if (p === "/api/metrics")
|
|
229
234
|
this.serveMetrics(res, url);
|
|
230
235
|
else if (p === "/api/tool-metrics")
|
|
@@ -269,6 +274,74 @@ class ViewerServer {
|
|
|
269
274
|
this.serveLogs(res, url);
|
|
270
275
|
else if (p === "/api/log-tools" && req.method === "GET")
|
|
271
276
|
this.serveLogTools(res);
|
|
277
|
+
else if (p === "/api/sharing/status" && req.method === "GET")
|
|
278
|
+
this.serveSharingStatus(res);
|
|
279
|
+
else if (p === "/api/sharing/pending-users" && req.method === "GET")
|
|
280
|
+
this.serveSharingPendingUsers(res);
|
|
281
|
+
else if (p === "/api/sharing/approve-user" && req.method === "POST")
|
|
282
|
+
this.handleSharingApproveUser(req, res);
|
|
283
|
+
else if (p === "/api/sharing/reject-user" && req.method === "POST")
|
|
284
|
+
this.handleSharingRejectUser(req, res);
|
|
285
|
+
else if (p === "/api/sharing/search/memories" && req.method === "POST")
|
|
286
|
+
this.handleSharingMemorySearch(req, res);
|
|
287
|
+
else if (p === "/api/sharing/memories/list" && req.method === "GET")
|
|
288
|
+
this.serveSharingMemoryList(res, url);
|
|
289
|
+
else if (p === "/api/sharing/tasks/list" && req.method === "GET")
|
|
290
|
+
this.serveSharingTaskList(res, url);
|
|
291
|
+
else if (p === "/api/sharing/skills/list" && req.method === "GET")
|
|
292
|
+
this.serveSharingSkillList(res, url);
|
|
293
|
+
else if (p === "/api/sharing/memory-detail" && req.method === "POST")
|
|
294
|
+
this.handleSharingMemoryDetail(req, res);
|
|
295
|
+
else if (p === "/api/sharing/search/skills" && req.method === "GET")
|
|
296
|
+
this.serveSharingSkillSearch(res, url);
|
|
297
|
+
else if (p === "/api/sharing/tasks/share" && req.method === "POST")
|
|
298
|
+
this.handleSharingTaskShare(req, res);
|
|
299
|
+
else if (p === "/api/sharing/tasks/unshare" && req.method === "POST")
|
|
300
|
+
this.handleSharingTaskUnshare(req, res);
|
|
301
|
+
else if (p === "/api/sharing/update-username" && req.method === "POST")
|
|
302
|
+
this.handleUpdateUsername(req, res);
|
|
303
|
+
else if (p === "/api/sharing/test-hub" && req.method === "POST")
|
|
304
|
+
this.handleTestHubConnection(req, res);
|
|
305
|
+
else if (p === "/api/sharing/memories/share" && req.method === "POST")
|
|
306
|
+
this.handleSharingMemoryShare(req, res);
|
|
307
|
+
else if (p === "/api/sharing/memories/unshare" && req.method === "POST")
|
|
308
|
+
this.handleSharingMemoryUnshare(req, res);
|
|
309
|
+
else if (p === "/api/sharing/skills/pull" && req.method === "POST")
|
|
310
|
+
this.handleSharingSkillPull(req, res);
|
|
311
|
+
else if (p === "/api/sharing/skills/share" && req.method === "POST")
|
|
312
|
+
this.handleSharingSkillShare(req, res);
|
|
313
|
+
else if (p === "/api/sharing/skills/unshare" && req.method === "POST")
|
|
314
|
+
this.handleSharingSkillUnshare(req, res);
|
|
315
|
+
else if (p === "/api/sharing/groups" && req.method === "GET")
|
|
316
|
+
this.serveSharingGroups(res);
|
|
317
|
+
else if (p === "/api/sharing/groups" && req.method === "POST")
|
|
318
|
+
this.handleSharingGroupCreate(req, res);
|
|
319
|
+
else if (p.match(/^\/api\/sharing\/groups\/[^/]+$/) && req.method === "PUT")
|
|
320
|
+
this.handleSharingGroupUpdate(req, res, p);
|
|
321
|
+
else if (p.match(/^\/api\/sharing\/groups\/[^/]+$/) && req.method === "DELETE")
|
|
322
|
+
this.handleSharingGroupDelete(res, p);
|
|
323
|
+
else if (p.match(/^\/api\/sharing\/groups\/[^/]+\/members$/) && req.method === "GET")
|
|
324
|
+
this.serveSharingGroupMembers(res, p);
|
|
325
|
+
else if (p.match(/^\/api\/sharing\/groups\/[^/]+\/members$/) && req.method === "POST")
|
|
326
|
+
this.handleSharingGroupAddMember(req, res, p);
|
|
327
|
+
else if (p.match(/^\/api\/sharing\/groups\/[^/]+\/members$/) && req.method === "DELETE")
|
|
328
|
+
this.handleSharingGroupRemoveMember(req, res, p);
|
|
329
|
+
else if (p === "/api/sharing/users" && req.method === "GET")
|
|
330
|
+
this.serveSharingUsers(res);
|
|
331
|
+
else if (p === "/api/admin/shared-tasks" && req.method === "GET")
|
|
332
|
+
this.serveAdminSharedTasks(res);
|
|
333
|
+
else if (p.match(/^\/api\/admin\/shared-tasks\/[^/]+$/) && req.method === "DELETE")
|
|
334
|
+
this.handleAdminDeleteTask(res, p);
|
|
335
|
+
else if (p === "/api/admin/shared-skills" && req.method === "GET")
|
|
336
|
+
this.serveAdminSharedSkills(res);
|
|
337
|
+
else if (p.match(/^\/api\/admin\/shared-skills\/[^/]+$/) && req.method === "DELETE")
|
|
338
|
+
this.handleAdminDeleteSkill(res, p);
|
|
339
|
+
else if (p === "/api/admin/shared-memories" && req.method === "GET")
|
|
340
|
+
this.serveAdminSharedMemories(res);
|
|
341
|
+
else if (p.match(/^\/api\/admin\/shared-memories\/[^/]+$/) && req.method === "DELETE")
|
|
342
|
+
this.handleAdminDeleteMemory(res, p);
|
|
343
|
+
else if (p === "/api/local-ips" && req.method === "GET")
|
|
344
|
+
this.serveLocalIPs(res);
|
|
272
345
|
else if (p === "/api/config" && req.method === "GET")
|
|
273
346
|
this.serveConfig(res);
|
|
274
347
|
else if (p === "/api/config" && req.method === "PUT")
|
|
@@ -454,15 +527,28 @@ class ViewerServer {
|
|
|
454
527
|
const totalRow = db.prepare("SELECT COUNT(*) as count FROM chunks" + where).get(...params);
|
|
455
528
|
const rawMemories = db.prepare("SELECT * FROM chunks" + where + ` ORDER BY created_at ${sortBy} LIMIT ? OFFSET ?`).all(...params, limit, offset);
|
|
456
529
|
const findMergeSources = db.prepare("SELECT id, summary, role FROM chunks WHERE dedup_target = ? AND (dedup_status = 'merged' OR dedup_status = 'duplicate')");
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
|
|
530
|
+
const chunkIds = rawMemories.map((m) => m.id);
|
|
531
|
+
const sharingMap = new Map();
|
|
532
|
+
if (chunkIds.length > 0) {
|
|
533
|
+
try {
|
|
534
|
+
const placeholders = chunkIds.map(() => "?").join(",");
|
|
535
|
+
const sharedRows = db.prepare(`SELECT source_chunk_id, visibility, group_id FROM hub_memories WHERE source_chunk_id IN (${placeholders})`).all(...chunkIds);
|
|
536
|
+
for (const r of sharedRows)
|
|
537
|
+
sharingMap.set(r.source_chunk_id, r);
|
|
538
|
+
}
|
|
539
|
+
catch {
|
|
460
540
|
}
|
|
461
|
-
|
|
541
|
+
}
|
|
542
|
+
const memories = rawMemories.map((m) => {
|
|
543
|
+
const out = m.role === "user" && m.content ? { ...m, content: (0, capture_1.stripInboundMetadata)(m.content) } : { ...m };
|
|
544
|
+
if (out.merge_count > 0) {
|
|
462
545
|
const sources = findMergeSources.all(m.id);
|
|
463
|
-
|
|
546
|
+
out.merge_sources = sources;
|
|
464
547
|
}
|
|
465
|
-
|
|
548
|
+
const shared = sharingMap.get(m.id);
|
|
549
|
+
out.sharingVisibility = shared?.visibility ?? null;
|
|
550
|
+
out.sharingGroupId = shared?.group_id ?? null;
|
|
551
|
+
return out;
|
|
466
552
|
});
|
|
467
553
|
this.store.recordViewerEvent("list");
|
|
468
554
|
this.jsonResponse(res, {
|
|
@@ -527,6 +613,7 @@ class ViewerServer {
|
|
|
527
613
|
}));
|
|
528
614
|
const db = this.store.db;
|
|
529
615
|
const meta = db.prepare("SELECT skill_status, skill_reason FROM tasks WHERE id = ?").get(taskId);
|
|
616
|
+
const sharedTask = db.prepare("SELECT visibility, group_id FROM hub_tasks WHERE source_task_id = ? ORDER BY updated_at DESC LIMIT 1").get(taskId);
|
|
530
617
|
this.jsonResponse(res, {
|
|
531
618
|
id: task.id,
|
|
532
619
|
sessionKey: task.sessionKey,
|
|
@@ -539,9 +626,11 @@ class ViewerServer {
|
|
|
539
626
|
skillStatus: meta?.skill_status ?? null,
|
|
540
627
|
skillReason: meta?.skill_reason ?? null,
|
|
541
628
|
skillLinks,
|
|
629
|
+
sharingVisibility: sharedTask?.visibility ?? null,
|
|
630
|
+
sharingGroupId: sharedTask?.group_id ?? null,
|
|
542
631
|
});
|
|
543
632
|
}
|
|
544
|
-
serveStats(res) {
|
|
633
|
+
serveStats(res, url) {
|
|
545
634
|
const emptyStats = {
|
|
546
635
|
totalMemories: 0, totalSessions: 0, totalEmbeddings: 0, totalSkills: 0,
|
|
547
636
|
embeddingProvider: this.embedder?.provider ?? "none",
|
|
@@ -553,6 +642,7 @@ class ViewerServer {
|
|
|
553
642
|
this.jsonResponse(res, emptyStats);
|
|
554
643
|
return;
|
|
555
644
|
}
|
|
645
|
+
const ownerFilter = url?.searchParams.get("owner") ?? "";
|
|
556
646
|
try {
|
|
557
647
|
const db = this.store.db;
|
|
558
648
|
const total = db.prepare("SELECT COUNT(*) as count FROM chunks").get();
|
|
@@ -571,7 +661,12 @@ class ViewerServer {
|
|
|
571
661
|
embCount = db.prepare("SELECT COUNT(*) as count FROM embeddings").get().count;
|
|
572
662
|
}
|
|
573
663
|
catch { /* table may not exist */ }
|
|
574
|
-
const
|
|
664
|
+
const sessionQuery = ownerFilter
|
|
665
|
+
? "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"
|
|
666
|
+
: "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";
|
|
667
|
+
const sessionList = (ownerFilter
|
|
668
|
+
? db.prepare(sessionQuery).all(ownerFilter)
|
|
669
|
+
: db.prepare(sessionQuery).all());
|
|
575
670
|
let skillCount = 0;
|
|
576
671
|
try {
|
|
577
672
|
skillCount = db.prepare("SELECT COUNT(*) as count FROM skills").get().count;
|
|
@@ -733,8 +828,10 @@ class ViewerServer {
|
|
|
733
828
|
const versions = this.store.getSkillVersions(skillId);
|
|
734
829
|
const relatedTasks = this.store.getTasksBySkill(skillId);
|
|
735
830
|
const files = node_fs_1.default.existsSync(skill.dirPath) ? this.walkDir(skill.dirPath, skill.dirPath) : [];
|
|
831
|
+
const db = this.store.db;
|
|
832
|
+
const sharedSkill = db.prepare("SELECT visibility, group_id FROM hub_skills WHERE source_skill_id = ? ORDER BY updated_at DESC LIMIT 1").get(skillId);
|
|
736
833
|
this.jsonResponse(res, {
|
|
737
|
-
skill,
|
|
834
|
+
skill: { ...skill, sharingVisibility: sharedSkill?.visibility ?? null, sharingGroupId: sharedSkill?.group_id ?? null },
|
|
738
835
|
versions: versions.map(v => ({
|
|
739
836
|
id: v.id,
|
|
740
837
|
version: v.version,
|
|
@@ -843,7 +940,7 @@ class ViewerServer {
|
|
|
843
940
|
handleSkillVisibility(req, res, urlPath) {
|
|
844
941
|
const segments = urlPath.split("/");
|
|
845
942
|
const skillId = segments[segments.length - 2];
|
|
846
|
-
this.readBody(req, (body) => {
|
|
943
|
+
this.readBody(req, async (body) => {
|
|
847
944
|
try {
|
|
848
945
|
const parsed = JSON.parse(body);
|
|
849
946
|
const visibility = parsed.visibility;
|
|
@@ -859,7 +956,47 @@ class ViewerServer {
|
|
|
859
956
|
return;
|
|
860
957
|
}
|
|
861
958
|
this.store.setSkillVisibility(skillId, visibility);
|
|
862
|
-
|
|
959
|
+
let hubSynced = false;
|
|
960
|
+
const sharing = this.ctx?.config?.sharing;
|
|
961
|
+
if (sharing?.enabled && this.ctx) {
|
|
962
|
+
try {
|
|
963
|
+
const hubClient = await this.resolveHubClientAware();
|
|
964
|
+
if (visibility === "public") {
|
|
965
|
+
const bundle = (0, skill_sync_1.buildSkillBundleForHub)(this.store, skillId);
|
|
966
|
+
const response = await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/publish", {
|
|
967
|
+
method: "POST",
|
|
968
|
+
body: JSON.stringify({ visibility: "public", groupId: null, metadata: bundle.metadata, bundle: bundle.bundle }),
|
|
969
|
+
});
|
|
970
|
+
if (hubClient.userId) {
|
|
971
|
+
const existing = this.store.getHubSkillBySource(hubClient.userId, skillId);
|
|
972
|
+
this.store.upsertHubSkill({
|
|
973
|
+
id: response?.skillId ?? existing?.id ?? node_crypto_1.default.randomUUID(),
|
|
974
|
+
sourceSkillId: skillId, sourceUserId: hubClient.userId,
|
|
975
|
+
name: skill.name, description: skill.description, version: skill.version,
|
|
976
|
+
groupId: null, visibility: "public",
|
|
977
|
+
bundle: JSON.stringify(bundle.bundle), qualityScore: skill.qualityScore,
|
|
978
|
+
createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(),
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
hubSynced = true;
|
|
982
|
+
this.log.info(`Skill "${skill.name}" published to Hub`);
|
|
983
|
+
}
|
|
984
|
+
else {
|
|
985
|
+
await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/unpublish", {
|
|
986
|
+
method: "POST",
|
|
987
|
+
body: JSON.stringify({ sourceSkillId: skillId }),
|
|
988
|
+
});
|
|
989
|
+
if (hubClient.userId)
|
|
990
|
+
this.store.deleteHubSkillBySource(hubClient.userId, skillId);
|
|
991
|
+
hubSynced = true;
|
|
992
|
+
this.log.info(`Skill "${skill.name}" unpublished from Hub`);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
catch (hubErr) {
|
|
996
|
+
this.log.warn(`Hub sync failed for skill visibility change: ${hubErr}`);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
this.jsonResponse(res, { ok: true, skillId, visibility, hubSynced });
|
|
863
1000
|
}
|
|
864
1001
|
catch (err) {
|
|
865
1002
|
const errMsg = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
|
|
@@ -1070,6 +1207,824 @@ class ViewerServer {
|
|
|
1070
1207
|
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
1071
1208
|
return node_path_1.default.join(home, ".openclaw", "openclaw.json");
|
|
1072
1209
|
}
|
|
1210
|
+
getPluginEntryConfig(raw) {
|
|
1211
|
+
const entries = raw?.plugins?.entries ?? {};
|
|
1212
|
+
return entries["memos-local-openclaw-plugin"]?.config
|
|
1213
|
+
?? entries["memos-lite-openclaw-plugin"]?.config
|
|
1214
|
+
?? entries["memos-lite"]?.config
|
|
1215
|
+
?? {};
|
|
1216
|
+
}
|
|
1217
|
+
getResolvedViewerConfig(raw) {
|
|
1218
|
+
const pluginCfg = this.getPluginEntryConfig(raw);
|
|
1219
|
+
const stateDir = this.ctx?.stateDir ?? this.getOpenClawHome();
|
|
1220
|
+
return (0, config_1.resolveConfig)(pluginCfg, stateDir);
|
|
1221
|
+
}
|
|
1222
|
+
hasUsableEmbeddingProvider(cfg) {
|
|
1223
|
+
const embedding = cfg.embedding;
|
|
1224
|
+
if (!embedding?.provider)
|
|
1225
|
+
return false;
|
|
1226
|
+
if (embedding.provider === "openclaw") {
|
|
1227
|
+
return !!(this.ctx?.openclawAPI) && embedding.capabilities?.hostEmbedding === true;
|
|
1228
|
+
}
|
|
1229
|
+
return true;
|
|
1230
|
+
}
|
|
1231
|
+
hasUsableSummarizerProvider(cfg) {
|
|
1232
|
+
const summarizer = cfg.summarizer;
|
|
1233
|
+
if (!summarizer?.provider)
|
|
1234
|
+
return false;
|
|
1235
|
+
if (summarizer.provider === "openclaw") {
|
|
1236
|
+
return !!(this.ctx?.openclawAPI) && summarizer.capabilities?.hostCompletion === true;
|
|
1237
|
+
}
|
|
1238
|
+
return true;
|
|
1239
|
+
}
|
|
1240
|
+
async serveSharingStatus(res) {
|
|
1241
|
+
const sharing = this.ctx?.config?.sharing;
|
|
1242
|
+
const persisted = this.store.getClientHubConnection();
|
|
1243
|
+
const resolvedHubUrl = sharing?.client?.hubAddress ? (0, hub_1.normalizeHubUrl)(sharing.client.hubAddress) : persisted?.hubUrl ?? null;
|
|
1244
|
+
const hasClientConfig = Boolean((sharing?.client?.hubAddress && sharing?.client?.userToken) ||
|
|
1245
|
+
(persisted?.hubUrl && persisted?.userToken));
|
|
1246
|
+
const base = {
|
|
1247
|
+
enabled: Boolean(sharing?.enabled),
|
|
1248
|
+
role: sharing?.role ?? null,
|
|
1249
|
+
clientConfigured: hasClientConfig,
|
|
1250
|
+
hubUrl: resolvedHubUrl,
|
|
1251
|
+
connection: { connected: false, user: null, hubUrl: undefined, teamName: null, apiVersion: null },
|
|
1252
|
+
admin: { canManageUsers: false, rejectSupported: false },
|
|
1253
|
+
};
|
|
1254
|
+
if (!this.ctx || !sharing?.enabled) {
|
|
1255
|
+
this.jsonResponse(res, base);
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
// Hub 模式下,本机就是管理者,直接赋予 admin 权限
|
|
1259
|
+
if (sharing.role === "hub") {
|
|
1260
|
+
base.admin.canManageUsers = true;
|
|
1261
|
+
base.admin.rejectSupported = true;
|
|
1262
|
+
base.connection.connected = true;
|
|
1263
|
+
base.connection.hubUrl = resolvedHubUrl ?? undefined;
|
|
1264
|
+
// 通过 hub API 获取 admin 用户的真实信息(含分组)
|
|
1265
|
+
let adminUser = { username: "hub-admin", role: "admin", groups: [] };
|
|
1266
|
+
try {
|
|
1267
|
+
const hub = this.resolveHubConnection();
|
|
1268
|
+
if (hub) {
|
|
1269
|
+
const me = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/me", { method: "GET" });
|
|
1270
|
+
if (me) {
|
|
1271
|
+
adminUser = {
|
|
1272
|
+
id: me.id,
|
|
1273
|
+
username: me.username ?? "hub-admin",
|
|
1274
|
+
role: me.role ?? "admin",
|
|
1275
|
+
groups: Array.isArray(me.groups) ? me.groups : [],
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
catch { /* fallback to default */ }
|
|
1281
|
+
base.connection.user = adminUser;
|
|
1282
|
+
// Fetch team info from own hub
|
|
1283
|
+
try {
|
|
1284
|
+
const selfUrl = resolvedHubUrl || `http://localhost:${sharing.hub?.port ?? 21816}`;
|
|
1285
|
+
const info = await fetch(`${selfUrl}/api/v1/hub/info`).then(r => r.ok ? r.json() : null).catch(() => null);
|
|
1286
|
+
base.connection.teamName = info?.teamName ?? sharing.hub?.teamName ?? null;
|
|
1287
|
+
base.connection.apiVersion = info?.apiVersion ?? null;
|
|
1288
|
+
}
|
|
1289
|
+
catch { /* ignore */ }
|
|
1290
|
+
this.jsonResponse(res, base);
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
if (!hasClientConfig) {
|
|
1294
|
+
this.jsonResponse(res, base);
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
try {
|
|
1298
|
+
const status = await (0, connector_1.getHubStatus)(this.store, this.ctx.config);
|
|
1299
|
+
const output = { ...base, connection: { ...base.connection, ...status } };
|
|
1300
|
+
if (status.connected && status.hubUrl) {
|
|
1301
|
+
try {
|
|
1302
|
+
const info = await fetch(`${status.hubUrl}/api/v1/hub/info`).then((r) => (r.ok ? r.json() : null)).catch(() => null);
|
|
1303
|
+
output.connection.teamName = info?.teamName ?? null;
|
|
1304
|
+
output.connection.apiVersion = info?.apiVersion ?? null;
|
|
1305
|
+
}
|
|
1306
|
+
catch { }
|
|
1307
|
+
}
|
|
1308
|
+
output.admin.canManageUsers = status.connected && status.user?.role === "admin";
|
|
1309
|
+
output.admin.rejectSupported = output.admin.canManageUsers;
|
|
1310
|
+
this.jsonResponse(res, output);
|
|
1311
|
+
}
|
|
1312
|
+
catch (err) {
|
|
1313
|
+
this.jsonResponse(res, { ...base, error: String(err) });
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
async serveSharingPendingUsers(res) {
|
|
1317
|
+
if (!this.ctx)
|
|
1318
|
+
return this.jsonResponse(res, { users: [], error: "sharing_unavailable" });
|
|
1319
|
+
try {
|
|
1320
|
+
const hub = this.resolveHubConnection();
|
|
1321
|
+
if (!hub)
|
|
1322
|
+
return this.jsonResponse(res, { users: [], error: "not_configured" });
|
|
1323
|
+
const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/pending-users", { method: "GET" });
|
|
1324
|
+
this.jsonResponse(res, { users: Array.isArray(data?.users) ? data.users : [] });
|
|
1325
|
+
}
|
|
1326
|
+
catch (err) {
|
|
1327
|
+
this.jsonResponse(res, { users: [], error: String(err) });
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
handleSharingApproveUser(req, res) {
|
|
1331
|
+
this.readBody(req, async (body) => {
|
|
1332
|
+
if (!this.ctx)
|
|
1333
|
+
return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
1334
|
+
try {
|
|
1335
|
+
const parsed = JSON.parse(body || "{}");
|
|
1336
|
+
const hub = this.resolveHubConnection();
|
|
1337
|
+
if (!hub)
|
|
1338
|
+
return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1339
|
+
const result = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/approve-user", {
|
|
1340
|
+
method: "POST",
|
|
1341
|
+
body: JSON.stringify({ userId: parsed.userId, username: parsed.username }),
|
|
1342
|
+
});
|
|
1343
|
+
this.jsonResponse(res, { ok: true, result });
|
|
1344
|
+
}
|
|
1345
|
+
catch (err) {
|
|
1346
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1347
|
+
}
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
handleSharingRejectUser(req, res) {
|
|
1351
|
+
this.readBody(req, async (body) => {
|
|
1352
|
+
if (!this.ctx)
|
|
1353
|
+
return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
1354
|
+
try {
|
|
1355
|
+
const parsed = JSON.parse(body || "{}");
|
|
1356
|
+
const hub = this.resolveHubConnection();
|
|
1357
|
+
if (!hub)
|
|
1358
|
+
return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1359
|
+
const result = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/reject-user", {
|
|
1360
|
+
method: "POST",
|
|
1361
|
+
body: JSON.stringify({ userId: parsed.userId }),
|
|
1362
|
+
});
|
|
1363
|
+
this.jsonResponse(res, { ok: true, result });
|
|
1364
|
+
}
|
|
1365
|
+
catch (err) {
|
|
1366
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1367
|
+
}
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
async serveSharingMemoryList(res, url) {
|
|
1371
|
+
if (!this.ctx)
|
|
1372
|
+
return this.jsonResponse(res, { memories: [], error: "sharing_unavailable" });
|
|
1373
|
+
try {
|
|
1374
|
+
const limit = Number(url.searchParams.get("limit") || 40);
|
|
1375
|
+
const data = await (0, hub_1.hubListMemories)(this.store, this.ctx, { limit });
|
|
1376
|
+
this.jsonResponse(res, { memories: Array.isArray(data?.memories) ? data.memories : [] });
|
|
1377
|
+
}
|
|
1378
|
+
catch (err) {
|
|
1379
|
+
this.jsonResponse(res, { memories: [], error: String(err) });
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
async serveSharingTaskList(res, url) {
|
|
1383
|
+
if (!this.ctx)
|
|
1384
|
+
return this.jsonResponse(res, { tasks: [], error: "sharing_unavailable" });
|
|
1385
|
+
try {
|
|
1386
|
+
const limit = Number(url.searchParams.get("limit") || 40);
|
|
1387
|
+
const data = await (0, hub_1.hubListTasks)(this.store, this.ctx, { limit });
|
|
1388
|
+
this.jsonResponse(res, { tasks: Array.isArray(data?.tasks) ? data.tasks : [] });
|
|
1389
|
+
}
|
|
1390
|
+
catch (err) {
|
|
1391
|
+
this.jsonResponse(res, { tasks: [], error: String(err) });
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
async serveSharingSkillList(res, url) {
|
|
1395
|
+
if (!this.ctx)
|
|
1396
|
+
return this.jsonResponse(res, { skills: [], error: "sharing_unavailable" });
|
|
1397
|
+
try {
|
|
1398
|
+
const limit = Number(url.searchParams.get("limit") || 40);
|
|
1399
|
+
const data = await (0, hub_1.hubListSkills)(this.store, this.ctx, { limit });
|
|
1400
|
+
this.jsonResponse(res, { skills: Array.isArray(data?.skills) ? data.skills : [] });
|
|
1401
|
+
}
|
|
1402
|
+
catch (err) {
|
|
1403
|
+
this.jsonResponse(res, { skills: [], error: String(err) });
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
handleSharingMemorySearch(req, res) {
|
|
1407
|
+
this.readBody(req, async (body) => {
|
|
1408
|
+
if (!this.ctx)
|
|
1409
|
+
return this.jsonResponse(res, { local: { hits: [], meta: {} }, hub: { hits: [], meta: { totalCandidates: 0, searchedGroups: [], includedPublic: false } }, error: "sharing_unavailable" });
|
|
1410
|
+
const emptyHub = { hits: [], meta: { totalCandidates: 0, searchedGroups: [], includedPublic: false } };
|
|
1411
|
+
try {
|
|
1412
|
+
const parsed = JSON.parse(body || "{}");
|
|
1413
|
+
const query = String(parsed.query || "");
|
|
1414
|
+
const role = typeof parsed.role === "string" ? parsed.role : undefined;
|
|
1415
|
+
const maxResults = typeof parsed.maxResults === "number" ? parsed.maxResults : 10;
|
|
1416
|
+
const scope = parsed.scope === "group" || parsed.scope === "all" ? parsed.scope : "local";
|
|
1417
|
+
const local = this.searchLocalViewerMemories(query, { role, maxResults });
|
|
1418
|
+
if (scope === "local") {
|
|
1419
|
+
return this.jsonResponse(res, { local: { hits: local.hits, meta: local.meta }, hub: emptyHub });
|
|
1420
|
+
}
|
|
1421
|
+
try {
|
|
1422
|
+
const hub = await (0, hub_1.hubSearchMemories)(this.store, this.ctx, { query, maxResults, scope });
|
|
1423
|
+
this.jsonResponse(res, { local: { hits: local.hits, meta: local.meta }, hub });
|
|
1424
|
+
}
|
|
1425
|
+
catch (err) {
|
|
1426
|
+
this.jsonResponse(res, { local: { hits: local.hits, meta: local.meta }, hub: emptyHub, error: String(err) });
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
catch (err) {
|
|
1430
|
+
this.jsonResponse(res, { local: { hits: [], meta: {} }, hub: emptyHub, error: String(err) });
|
|
1431
|
+
}
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
handleSharingMemoryDetail(req, res) {
|
|
1435
|
+
this.readBody(req, async (body) => {
|
|
1436
|
+
if (!this.ctx)
|
|
1437
|
+
return this.jsonResponse(res, { error: "sharing_unavailable" });
|
|
1438
|
+
try {
|
|
1439
|
+
const parsed = JSON.parse(body || "{}");
|
|
1440
|
+
const detail = await (0, hub_1.hubGetMemoryDetail)(this.store, this.ctx, { remoteHitId: String(parsed.remoteHitId || "") });
|
|
1441
|
+
this.jsonResponse(res, detail);
|
|
1442
|
+
}
|
|
1443
|
+
catch (err) {
|
|
1444
|
+
this.jsonResponse(res, { error: String(err) });
|
|
1445
|
+
}
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
async serveSharingSkillSearch(res, url) {
|
|
1449
|
+
if (!this.ctx)
|
|
1450
|
+
return this.jsonResponse(res, { local: { hits: [] }, hub: { hits: [] }, error: "sharing_unavailable" });
|
|
1451
|
+
try {
|
|
1452
|
+
const query = String(url.searchParams.get("query") || "");
|
|
1453
|
+
const scope = url.searchParams.get("scope") === "group" || url.searchParams.get("scope") === "all" ? url.searchParams.get("scope") : "local";
|
|
1454
|
+
const recall = new engine_1.RecallEngine(this.store, this.embedder, this.ctx);
|
|
1455
|
+
const localHits = await recall.searchSkills(query, "mix", "agent:main");
|
|
1456
|
+
if (scope === "local") {
|
|
1457
|
+
return this.jsonResponse(res, { local: { hits: localHits }, hub: { hits: [] } });
|
|
1458
|
+
}
|
|
1459
|
+
try {
|
|
1460
|
+
const hub = await (0, hub_1.hubSearchSkills)(this.store, this.ctx, { query, maxResults: Number(url.searchParams.get("maxResults") || 20) });
|
|
1461
|
+
this.jsonResponse(res, { local: { hits: localHits }, hub });
|
|
1462
|
+
}
|
|
1463
|
+
catch (err) {
|
|
1464
|
+
this.jsonResponse(res, { local: { hits: localHits }, hub: { hits: [] }, error: String(err) });
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
catch (err) {
|
|
1468
|
+
this.jsonResponse(res, { local: { hits: [] }, hub: { hits: [] }, error: String(err) });
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
searchLocalViewerMemories(query, options) {
|
|
1472
|
+
const db = this.store.db;
|
|
1473
|
+
const role = options?.role;
|
|
1474
|
+
const maxResults = options?.maxResults ?? 10;
|
|
1475
|
+
const params = [];
|
|
1476
|
+
let rows = [];
|
|
1477
|
+
try {
|
|
1478
|
+
let sql = "SELECT c.* FROM chunks_fts f JOIN chunks c ON f.rowid = c.rowid WHERE chunks_fts MATCH ?";
|
|
1479
|
+
params.push(query);
|
|
1480
|
+
if (role) {
|
|
1481
|
+
sql += " AND c.role = ?";
|
|
1482
|
+
params.push(role);
|
|
1483
|
+
}
|
|
1484
|
+
sql += " ORDER BY rank LIMIT ?";
|
|
1485
|
+
params.push(maxResults);
|
|
1486
|
+
rows = db.prepare(sql).all(...params);
|
|
1487
|
+
}
|
|
1488
|
+
catch {
|
|
1489
|
+
const likeParams = [`%${query}%`, `%${query}%`];
|
|
1490
|
+
let sql = "SELECT * FROM chunks WHERE (content LIKE ? OR summary LIKE ?)";
|
|
1491
|
+
if (role) {
|
|
1492
|
+
sql += " AND role = ?";
|
|
1493
|
+
likeParams.push(role);
|
|
1494
|
+
}
|
|
1495
|
+
sql += " ORDER BY created_at DESC LIMIT ?";
|
|
1496
|
+
likeParams.push(maxResults);
|
|
1497
|
+
rows = db.prepare(sql).all(...likeParams);
|
|
1498
|
+
}
|
|
1499
|
+
const hits = rows.map((row, idx) => ({
|
|
1500
|
+
id: row.id,
|
|
1501
|
+
summary: row.summary || row.content?.slice(0, 120) || "",
|
|
1502
|
+
excerpt: row.content || "",
|
|
1503
|
+
score: Math.max(0.3, 1 - idx * 0.1),
|
|
1504
|
+
role: row.role,
|
|
1505
|
+
ref: { sessionKey: row.session_key, chunkId: row.id, turnId: row.turn_id, seq: row.seq },
|
|
1506
|
+
taskId: row.task_id ?? null,
|
|
1507
|
+
skillId: row.skill_id ?? null,
|
|
1508
|
+
}));
|
|
1509
|
+
return { hits, meta: { total: hits.length, usedMaxResults: maxResults } };
|
|
1510
|
+
}
|
|
1511
|
+
handleSharingTaskShare(req, res) {
|
|
1512
|
+
this.readBody(req, async (body) => {
|
|
1513
|
+
if (!this.ctx)
|
|
1514
|
+
return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
1515
|
+
try {
|
|
1516
|
+
const parsed = JSON.parse(body || "{}");
|
|
1517
|
+
const taskId = String(parsed.taskId || "");
|
|
1518
|
+
const visibility = parsed.visibility === "group" ? "group" : "public";
|
|
1519
|
+
const groupId = typeof parsed.groupId === "string" ? parsed.groupId : undefined;
|
|
1520
|
+
const task = this.store.getTask(taskId);
|
|
1521
|
+
if (!task)
|
|
1522
|
+
return this.jsonResponse(res, { ok: false, error: "task_not_found" });
|
|
1523
|
+
const chunks = this.store.getChunksByTask(taskId);
|
|
1524
|
+
if (chunks.length === 0)
|
|
1525
|
+
return this.jsonResponse(res, { ok: false, error: "no_chunks" });
|
|
1526
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1527
|
+
const response = await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/share", {
|
|
1528
|
+
method: "POST",
|
|
1529
|
+
body: JSON.stringify({
|
|
1530
|
+
task: {
|
|
1531
|
+
id: task.id,
|
|
1532
|
+
sourceTaskId: task.id,
|
|
1533
|
+
title: task.title,
|
|
1534
|
+
summary: task.summary,
|
|
1535
|
+
groupId: visibility === "group" ? groupId ?? null : null,
|
|
1536
|
+
visibility,
|
|
1537
|
+
createdAt: task.startedAt ?? Date.now(),
|
|
1538
|
+
updatedAt: task.updatedAt ?? Date.now(),
|
|
1539
|
+
},
|
|
1540
|
+
chunks: chunks.map((chunk) => ({
|
|
1541
|
+
id: chunk.id,
|
|
1542
|
+
hubTaskId: task.id,
|
|
1543
|
+
sourceTaskId: task.id,
|
|
1544
|
+
sourceChunkId: chunk.id,
|
|
1545
|
+
role: chunk.role,
|
|
1546
|
+
content: chunk.content,
|
|
1547
|
+
summary: chunk.summary,
|
|
1548
|
+
kind: chunk.kind,
|
|
1549
|
+
createdAt: chunk.createdAt,
|
|
1550
|
+
})),
|
|
1551
|
+
}),
|
|
1552
|
+
});
|
|
1553
|
+
const hubUserId = hubClient.userId;
|
|
1554
|
+
if (hubUserId) {
|
|
1555
|
+
this.store.upsertHubTask({
|
|
1556
|
+
id: task.id,
|
|
1557
|
+
sourceTaskId: task.id,
|
|
1558
|
+
sourceUserId: hubUserId,
|
|
1559
|
+
title: task.title,
|
|
1560
|
+
summary: task.summary,
|
|
1561
|
+
groupId: visibility === "group" ? groupId ?? null : null,
|
|
1562
|
+
visibility,
|
|
1563
|
+
createdAt: task.startedAt ?? Date.now(),
|
|
1564
|
+
updatedAt: task.updatedAt ?? Date.now(),
|
|
1565
|
+
});
|
|
1566
|
+
}
|
|
1567
|
+
this.jsonResponse(res, { ok: true, taskId, visibility, response });
|
|
1568
|
+
}
|
|
1569
|
+
catch (err) {
|
|
1570
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1571
|
+
}
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
handleSharingTaskUnshare(req, res) {
|
|
1575
|
+
this.readBody(req, async (body) => {
|
|
1576
|
+
if (!this.ctx)
|
|
1577
|
+
return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
1578
|
+
try {
|
|
1579
|
+
const parsed = JSON.parse(body || "{}");
|
|
1580
|
+
const taskId = String(parsed.taskId || "");
|
|
1581
|
+
const task = this.store.getTask(taskId);
|
|
1582
|
+
if (!task)
|
|
1583
|
+
return this.jsonResponse(res, { ok: false, error: "task_not_found" });
|
|
1584
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1585
|
+
await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/unshare", {
|
|
1586
|
+
method: "POST",
|
|
1587
|
+
body: JSON.stringify({ sourceTaskId: task.id }),
|
|
1588
|
+
});
|
|
1589
|
+
const hubUserId = hubClient.userId;
|
|
1590
|
+
if (hubUserId)
|
|
1591
|
+
this.store.deleteHubTaskBySource(hubUserId, task.id);
|
|
1592
|
+
this.jsonResponse(res, { ok: true, taskId });
|
|
1593
|
+
}
|
|
1594
|
+
catch (err) {
|
|
1595
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1596
|
+
}
|
|
1597
|
+
});
|
|
1598
|
+
}
|
|
1599
|
+
handleSharingMemoryShare(req, res) {
|
|
1600
|
+
this.readBody(req, async (body) => {
|
|
1601
|
+
if (!this.ctx)
|
|
1602
|
+
return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
1603
|
+
try {
|
|
1604
|
+
const parsed = JSON.parse(body || "{}");
|
|
1605
|
+
const chunkId = String(parsed.chunkId || "");
|
|
1606
|
+
const visibility = parsed.visibility === "group" ? "group" : "public";
|
|
1607
|
+
const groupId = typeof parsed.groupId === "string" ? parsed.groupId : undefined;
|
|
1608
|
+
const db = this.store.db;
|
|
1609
|
+
const chunk = db.prepare("SELECT * FROM chunks WHERE id = ?").get(chunkId);
|
|
1610
|
+
if (!chunk)
|
|
1611
|
+
return this.jsonResponse(res, { ok: false, error: "memory_not_found" });
|
|
1612
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1613
|
+
const response = await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/share", {
|
|
1614
|
+
method: "POST",
|
|
1615
|
+
body: JSON.stringify({
|
|
1616
|
+
memory: {
|
|
1617
|
+
sourceChunkId: chunk.id,
|
|
1618
|
+
role: chunk.role,
|
|
1619
|
+
content: chunk.content,
|
|
1620
|
+
summary: chunk.summary,
|
|
1621
|
+
kind: chunk.kind,
|
|
1622
|
+
groupId: visibility === "group" ? groupId ?? null : null,
|
|
1623
|
+
visibility,
|
|
1624
|
+
},
|
|
1625
|
+
}),
|
|
1626
|
+
});
|
|
1627
|
+
const hubUserId = hubClient.userId;
|
|
1628
|
+
if (hubUserId) {
|
|
1629
|
+
const now = Date.now();
|
|
1630
|
+
const existing = this.store.getHubMemoryBySource(hubUserId, chunk.id);
|
|
1631
|
+
this.store.upsertHubMemory({
|
|
1632
|
+
id: response?.memoryId ?? existing?.id ?? node_crypto_1.default.randomUUID(),
|
|
1633
|
+
sourceChunkId: chunk.id,
|
|
1634
|
+
sourceUserId: hubUserId,
|
|
1635
|
+
role: chunk.role,
|
|
1636
|
+
content: chunk.content,
|
|
1637
|
+
summary: chunk.summary ?? "",
|
|
1638
|
+
kind: chunk.kind,
|
|
1639
|
+
groupId: visibility === "group" ? groupId ?? null : null,
|
|
1640
|
+
visibility,
|
|
1641
|
+
createdAt: existing?.createdAt ?? now,
|
|
1642
|
+
updatedAt: now,
|
|
1643
|
+
});
|
|
1644
|
+
}
|
|
1645
|
+
this.jsonResponse(res, { ok: true, chunkId, visibility, response });
|
|
1646
|
+
}
|
|
1647
|
+
catch (err) {
|
|
1648
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1649
|
+
}
|
|
1650
|
+
});
|
|
1651
|
+
}
|
|
1652
|
+
handleSharingMemoryUnshare(req, res) {
|
|
1653
|
+
this.readBody(req, async (body) => {
|
|
1654
|
+
if (!this.ctx)
|
|
1655
|
+
return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
1656
|
+
try {
|
|
1657
|
+
const parsed = JSON.parse(body || "{}");
|
|
1658
|
+
const chunkId = String(parsed.chunkId || "");
|
|
1659
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1660
|
+
await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/unshare", {
|
|
1661
|
+
method: "POST",
|
|
1662
|
+
body: JSON.stringify({ sourceChunkId: chunkId }),
|
|
1663
|
+
});
|
|
1664
|
+
const hubUserId = hubClient.userId;
|
|
1665
|
+
if (hubUserId)
|
|
1666
|
+
this.store.deleteHubMemoryBySource(hubUserId, chunkId);
|
|
1667
|
+
this.jsonResponse(res, { ok: true, chunkId });
|
|
1668
|
+
}
|
|
1669
|
+
catch (err) {
|
|
1670
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1671
|
+
}
|
|
1672
|
+
});
|
|
1673
|
+
}
|
|
1674
|
+
handleSharingSkillPull(req, res) {
|
|
1675
|
+
this.readBody(req, async (body) => {
|
|
1676
|
+
if (!this.ctx)
|
|
1677
|
+
return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
1678
|
+
try {
|
|
1679
|
+
const parsed = JSON.parse(body || "{}");
|
|
1680
|
+
const skillId = String(parsed.skillId || "");
|
|
1681
|
+
const payload = await (0, skill_sync_1.fetchHubSkillBundle)(this.store, this.ctx, { skillId });
|
|
1682
|
+
const restored = (0, skill_sync_1.restoreSkillBundleFromHub)(this.store, this.ctx, payload);
|
|
1683
|
+
this.jsonResponse(res, { ok: true, pulled: true, hubSkillId: skillId, ...restored });
|
|
1684
|
+
}
|
|
1685
|
+
catch (err) {
|
|
1686
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1687
|
+
}
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
handleSharingSkillShare(req, res) {
|
|
1691
|
+
this.readBody(req, async (body) => {
|
|
1692
|
+
if (!this.ctx)
|
|
1693
|
+
return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
1694
|
+
try {
|
|
1695
|
+
const parsed = JSON.parse(body || "{}");
|
|
1696
|
+
const skillId = String(parsed.skillId || "");
|
|
1697
|
+
const visibility = parsed.visibility === "group" ? "group" : "public";
|
|
1698
|
+
const groupId = parsed.groupId ? String(parsed.groupId) : null;
|
|
1699
|
+
const skill = this.store.getSkill(skillId);
|
|
1700
|
+
if (!skill)
|
|
1701
|
+
return this.jsonResponse(res, { ok: false, error: "skill_not_found" });
|
|
1702
|
+
const bundle = (0, skill_sync_1.buildSkillBundleForHub)(this.store, skillId);
|
|
1703
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1704
|
+
const response = await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/publish", {
|
|
1705
|
+
method: "POST",
|
|
1706
|
+
body: JSON.stringify({
|
|
1707
|
+
visibility,
|
|
1708
|
+
groupId: visibility === "group" ? groupId : null,
|
|
1709
|
+
metadata: bundle.metadata,
|
|
1710
|
+
bundle: bundle.bundle,
|
|
1711
|
+
}),
|
|
1712
|
+
});
|
|
1713
|
+
const hubUserId = hubClient.userId;
|
|
1714
|
+
if (hubUserId) {
|
|
1715
|
+
const existing = this.store.getHubSkillBySource(hubUserId, skillId);
|
|
1716
|
+
this.store.upsertHubSkill({
|
|
1717
|
+
id: response?.skillId ?? existing?.id ?? node_crypto_1.default.randomUUID(),
|
|
1718
|
+
sourceSkillId: skillId,
|
|
1719
|
+
sourceUserId: hubUserId,
|
|
1720
|
+
name: skill.name,
|
|
1721
|
+
description: skill.description,
|
|
1722
|
+
version: skill.version,
|
|
1723
|
+
groupId: visibility === "group" ? groupId : null,
|
|
1724
|
+
visibility,
|
|
1725
|
+
bundle: JSON.stringify(bundle.bundle),
|
|
1726
|
+
qualityScore: skill.qualityScore,
|
|
1727
|
+
createdAt: existing?.createdAt ?? Date.now(),
|
|
1728
|
+
updatedAt: Date.now(),
|
|
1729
|
+
});
|
|
1730
|
+
}
|
|
1731
|
+
this.jsonResponse(res, { ok: true, skillId, visibility, response });
|
|
1732
|
+
}
|
|
1733
|
+
catch (err) {
|
|
1734
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1735
|
+
}
|
|
1736
|
+
});
|
|
1737
|
+
}
|
|
1738
|
+
handleSharingSkillUnshare(req, res) {
|
|
1739
|
+
this.readBody(req, async (body) => {
|
|
1740
|
+
if (!this.ctx)
|
|
1741
|
+
return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
1742
|
+
try {
|
|
1743
|
+
const parsed = JSON.parse(body || "{}");
|
|
1744
|
+
const skillId = String(parsed.skillId || "");
|
|
1745
|
+
const skill = this.store.getSkill(skillId);
|
|
1746
|
+
if (!skill)
|
|
1747
|
+
return this.jsonResponse(res, { ok: false, error: "skill_not_found" });
|
|
1748
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1749
|
+
await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/unpublish", {
|
|
1750
|
+
method: "POST",
|
|
1751
|
+
body: JSON.stringify({ sourceSkillId: skill.id }),
|
|
1752
|
+
});
|
|
1753
|
+
const hubUserId = hubClient.userId;
|
|
1754
|
+
if (hubUserId)
|
|
1755
|
+
this.store.deleteHubSkillBySource(hubUserId, skill.id);
|
|
1756
|
+
this.jsonResponse(res, { ok: true, skillId });
|
|
1757
|
+
}
|
|
1758
|
+
catch (err) {
|
|
1759
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1760
|
+
}
|
|
1761
|
+
});
|
|
1762
|
+
}
|
|
1763
|
+
resolveHubConnection() {
|
|
1764
|
+
if (!this.ctx)
|
|
1765
|
+
return null;
|
|
1766
|
+
// Hub 模式:连接自己,用 bootstrap admin token
|
|
1767
|
+
const sharing = this.ctx.config.sharing;
|
|
1768
|
+
if (sharing?.role === "hub") {
|
|
1769
|
+
const hubPort = sharing.hub?.port ?? 18800;
|
|
1770
|
+
const hubUrl = `http://127.0.0.1:${hubPort}`;
|
|
1771
|
+
try {
|
|
1772
|
+
const authPath = node_path_1.default.join(this.dataDir, "hub-auth.json");
|
|
1773
|
+
const authData = JSON.parse(node_fs_1.default.readFileSync(authPath, "utf8"));
|
|
1774
|
+
const adminToken = authData?.bootstrapAdminToken;
|
|
1775
|
+
if (adminToken)
|
|
1776
|
+
return { hubUrl, userToken: adminToken };
|
|
1777
|
+
}
|
|
1778
|
+
catch {
|
|
1779
|
+
// hub-auth.json 不存在或读取失败,fall through
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
// Client 模式:用配置的 hubAddress + userToken
|
|
1783
|
+
const conn = this.store.getClientHubConnection();
|
|
1784
|
+
const hubUrl = conn?.hubUrl || this.ctx.config.sharing?.client?.hubAddress || "";
|
|
1785
|
+
const userToken = conn?.userToken || this.ctx.config.sharing?.client?.userToken || "";
|
|
1786
|
+
if (!hubUrl || !userToken)
|
|
1787
|
+
return null;
|
|
1788
|
+
return { hubUrl: (0, hub_1.normalizeHubUrl)(hubUrl), userToken };
|
|
1789
|
+
}
|
|
1790
|
+
/** resolveHubClient 的 viewer 版本:hub 模式下使用 bootstrap admin 身份 */
|
|
1791
|
+
async resolveHubClientAware() {
|
|
1792
|
+
if (!this.ctx)
|
|
1793
|
+
throw new Error("sharing_unavailable");
|
|
1794
|
+
const sharing = this.ctx.config.sharing;
|
|
1795
|
+
if (sharing?.role === "hub") {
|
|
1796
|
+
const hub = this.resolveHubConnection();
|
|
1797
|
+
if (hub) {
|
|
1798
|
+
const me = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/me", { method: "GET" });
|
|
1799
|
+
return {
|
|
1800
|
+
hubUrl: hub.hubUrl,
|
|
1801
|
+
userToken: hub.userToken,
|
|
1802
|
+
userId: String(me.id),
|
|
1803
|
+
username: String(me.username ?? "hub-admin"),
|
|
1804
|
+
role: String(me.role ?? "admin"),
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
return (0, hub_1.resolveHubClient)(this.store, this.ctx);
|
|
1809
|
+
}
|
|
1810
|
+
extractGroupId(path) {
|
|
1811
|
+
const m = path.match(/\/api\/sharing\/groups\/([^/]+)/);
|
|
1812
|
+
return m ? decodeURIComponent(m[1]) : "";
|
|
1813
|
+
}
|
|
1814
|
+
async serveSharingGroups(res) {
|
|
1815
|
+
const hub = this.resolveHubConnection();
|
|
1816
|
+
if (!hub)
|
|
1817
|
+
return this.jsonResponse(res, { groups: [], error: "not_configured" });
|
|
1818
|
+
try {
|
|
1819
|
+
const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/groups", { method: "GET" });
|
|
1820
|
+
this.jsonResponse(res, { groups: Array.isArray(data?.groups) ? data.groups : [] });
|
|
1821
|
+
}
|
|
1822
|
+
catch (err) {
|
|
1823
|
+
this.jsonResponse(res, { groups: [], error: String(err) });
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
handleSharingGroupCreate(req, res) {
|
|
1827
|
+
this.readBody(req, async (body) => {
|
|
1828
|
+
const hub = this.resolveHubConnection();
|
|
1829
|
+
if (!hub)
|
|
1830
|
+
return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1831
|
+
try {
|
|
1832
|
+
const parsed = JSON.parse(body || "{}");
|
|
1833
|
+
const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/groups", {
|
|
1834
|
+
method: "POST",
|
|
1835
|
+
body: JSON.stringify({ name: parsed.name, description: parsed.description }),
|
|
1836
|
+
});
|
|
1837
|
+
this.jsonResponse(res, { ok: true, ...data });
|
|
1838
|
+
}
|
|
1839
|
+
catch (err) {
|
|
1840
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1841
|
+
}
|
|
1842
|
+
});
|
|
1843
|
+
}
|
|
1844
|
+
handleSharingGroupUpdate(req, res, p) {
|
|
1845
|
+
this.readBody(req, async (body) => {
|
|
1846
|
+
const hub = this.resolveHubConnection();
|
|
1847
|
+
if (!hub)
|
|
1848
|
+
return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1849
|
+
const groupId = this.extractGroupId(p);
|
|
1850
|
+
try {
|
|
1851
|
+
const parsed = JSON.parse(body || "{}");
|
|
1852
|
+
await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}`, {
|
|
1853
|
+
method: "PUT",
|
|
1854
|
+
body: JSON.stringify({ name: parsed.name, description: parsed.description }),
|
|
1855
|
+
});
|
|
1856
|
+
this.jsonResponse(res, { ok: true });
|
|
1857
|
+
}
|
|
1858
|
+
catch (err) {
|
|
1859
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1860
|
+
}
|
|
1861
|
+
});
|
|
1862
|
+
}
|
|
1863
|
+
async handleSharingGroupDelete(res, p) {
|
|
1864
|
+
const hub = this.resolveHubConnection();
|
|
1865
|
+
if (!hub)
|
|
1866
|
+
return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1867
|
+
const groupId = this.extractGroupId(p);
|
|
1868
|
+
try {
|
|
1869
|
+
await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}`, { method: "DELETE" });
|
|
1870
|
+
this.jsonResponse(res, { ok: true });
|
|
1871
|
+
}
|
|
1872
|
+
catch (err) {
|
|
1873
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
async serveSharingGroupMembers(res, p) {
|
|
1877
|
+
const hub = this.resolveHubConnection();
|
|
1878
|
+
if (!hub)
|
|
1879
|
+
return this.jsonResponse(res, { members: [], error: "not_configured" });
|
|
1880
|
+
const groupId = this.extractGroupId(p);
|
|
1881
|
+
try {
|
|
1882
|
+
const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}`, { method: "GET" });
|
|
1883
|
+
this.jsonResponse(res, { members: Array.isArray(data?.members) ? data.members : [] });
|
|
1884
|
+
}
|
|
1885
|
+
catch (err) {
|
|
1886
|
+
this.jsonResponse(res, { members: [], error: String(err) });
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
handleSharingGroupAddMember(req, res, p) {
|
|
1890
|
+
this.readBody(req, async (body) => {
|
|
1891
|
+
const hub = this.resolveHubConnection();
|
|
1892
|
+
if (!hub)
|
|
1893
|
+
return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1894
|
+
const groupId = this.extractGroupId(p);
|
|
1895
|
+
try {
|
|
1896
|
+
const parsed = JSON.parse(body || "{}");
|
|
1897
|
+
await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}/members`, {
|
|
1898
|
+
method: "POST",
|
|
1899
|
+
body: JSON.stringify({ userId: parsed.userId }),
|
|
1900
|
+
});
|
|
1901
|
+
this.jsonResponse(res, { ok: true });
|
|
1902
|
+
}
|
|
1903
|
+
catch (err) {
|
|
1904
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1905
|
+
}
|
|
1906
|
+
});
|
|
1907
|
+
}
|
|
1908
|
+
handleSharingGroupRemoveMember(req, res, p) {
|
|
1909
|
+
this.readBody(req, async (body) => {
|
|
1910
|
+
const hub = this.resolveHubConnection();
|
|
1911
|
+
if (!hub)
|
|
1912
|
+
return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1913
|
+
const groupId = this.extractGroupId(p);
|
|
1914
|
+
try {
|
|
1915
|
+
const parsed = JSON.parse(body || "{}");
|
|
1916
|
+
await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}/members`, {
|
|
1917
|
+
method: "DELETE",
|
|
1918
|
+
body: JSON.stringify({ userId: parsed.userId }),
|
|
1919
|
+
});
|
|
1920
|
+
this.jsonResponse(res, { ok: true });
|
|
1921
|
+
}
|
|
1922
|
+
catch (err) {
|
|
1923
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1924
|
+
}
|
|
1925
|
+
});
|
|
1926
|
+
}
|
|
1927
|
+
async serveSharingUsers(res) {
|
|
1928
|
+
const hub = this.resolveHubConnection();
|
|
1929
|
+
if (!hub)
|
|
1930
|
+
return this.jsonResponse(res, { users: [], error: "not_configured" });
|
|
1931
|
+
try {
|
|
1932
|
+
const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/users", { method: "GET" });
|
|
1933
|
+
this.jsonResponse(res, { users: Array.isArray(data?.users) ? data.users : [] });
|
|
1934
|
+
}
|
|
1935
|
+
catch (err) {
|
|
1936
|
+
this.jsonResponse(res, { users: [], error: String(err) });
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
// ─── Admin management endpoints (Hub-side data) ───
|
|
1940
|
+
async serveAdminSharedTasks(res) {
|
|
1941
|
+
const hub = this.resolveHubConnection();
|
|
1942
|
+
if (!hub)
|
|
1943
|
+
return this.jsonResponse(res, { tasks: [], error: "not_configured" });
|
|
1944
|
+
try {
|
|
1945
|
+
const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/shared-tasks", { method: "GET" });
|
|
1946
|
+
this.jsonResponse(res, { tasks: Array.isArray(data?.tasks) ? data.tasks : [] });
|
|
1947
|
+
}
|
|
1948
|
+
catch (err) {
|
|
1949
|
+
this.jsonResponse(res, { tasks: [], error: String(err) });
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
async handleAdminDeleteTask(res, p) {
|
|
1953
|
+
const hub = this.resolveHubConnection();
|
|
1954
|
+
if (!hub)
|
|
1955
|
+
return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1956
|
+
const taskId = decodeURIComponent(p.replace("/api/admin/shared-tasks/", ""));
|
|
1957
|
+
try {
|
|
1958
|
+
await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/admin/shared-tasks/${encodeURIComponent(taskId)}`, { method: "DELETE" });
|
|
1959
|
+
this.jsonResponse(res, { ok: true });
|
|
1960
|
+
}
|
|
1961
|
+
catch (err) {
|
|
1962
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
async serveAdminSharedSkills(res) {
|
|
1966
|
+
const hub = this.resolveHubConnection();
|
|
1967
|
+
if (!hub)
|
|
1968
|
+
return this.jsonResponse(res, { skills: [], error: "not_configured" });
|
|
1969
|
+
try {
|
|
1970
|
+
const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/shared-skills", { method: "GET" });
|
|
1971
|
+
this.jsonResponse(res, { skills: Array.isArray(data?.skills) ? data.skills : [] });
|
|
1972
|
+
}
|
|
1973
|
+
catch (err) {
|
|
1974
|
+
this.jsonResponse(res, { skills: [], error: String(err) });
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
async handleAdminDeleteSkill(res, p) {
|
|
1978
|
+
const hub = this.resolveHubConnection();
|
|
1979
|
+
if (!hub)
|
|
1980
|
+
return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1981
|
+
const skillId = decodeURIComponent(p.replace("/api/admin/shared-skills/", ""));
|
|
1982
|
+
try {
|
|
1983
|
+
await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/admin/shared-skills/${encodeURIComponent(skillId)}`, { method: "DELETE" });
|
|
1984
|
+
this.jsonResponse(res, { ok: true });
|
|
1985
|
+
}
|
|
1986
|
+
catch (err) {
|
|
1987
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
async serveAdminSharedMemories(res) {
|
|
1991
|
+
const hub = this.resolveHubConnection();
|
|
1992
|
+
if (!hub)
|
|
1993
|
+
return this.jsonResponse(res, { memories: [], error: "not_configured" });
|
|
1994
|
+
try {
|
|
1995
|
+
const data = await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/shared-memories", { method: "GET" });
|
|
1996
|
+
this.jsonResponse(res, { memories: Array.isArray(data?.memories) ? data.memories : [] });
|
|
1997
|
+
}
|
|
1998
|
+
catch (err) {
|
|
1999
|
+
this.jsonResponse(res, { memories: [], error: String(err) });
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
async handleAdminDeleteMemory(res, p) {
|
|
2003
|
+
const hub = this.resolveHubConnection();
|
|
2004
|
+
if (!hub)
|
|
2005
|
+
return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
2006
|
+
const memoryId = decodeURIComponent(p.replace("/api/admin/shared-memories/", ""));
|
|
2007
|
+
try {
|
|
2008
|
+
await (0, hub_1.hubRequestJson)(hub.hubUrl, hub.userToken, `/api/v1/hub/admin/shared-memories/${encodeURIComponent(memoryId)}`, { method: "DELETE" });
|
|
2009
|
+
this.jsonResponse(res, { ok: true });
|
|
2010
|
+
}
|
|
2011
|
+
catch (err) {
|
|
2012
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
serveLocalIPs(res) {
|
|
2016
|
+
const nets = node_os_1.default.networkInterfaces();
|
|
2017
|
+
const ips = [];
|
|
2018
|
+
for (const name of Object.keys(nets)) {
|
|
2019
|
+
for (const net of nets[name] ?? []) {
|
|
2020
|
+
if (net.family === "IPv4" && !net.internal) {
|
|
2021
|
+
ips.push(net.address);
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2026
|
+
res.end(JSON.stringify({ ips }));
|
|
2027
|
+
}
|
|
1073
2028
|
serveConfig(res) {
|
|
1074
2029
|
try {
|
|
1075
2030
|
const cfgPath = this.getOpenClawConfigPath();
|
|
@@ -1090,7 +2045,10 @@ class ViewerServer {
|
|
|
1090
2045
|
?? entries["memos-lite-openclaw-plugin"]
|
|
1091
2046
|
?? entries["memos-lite"]
|
|
1092
2047
|
?? {};
|
|
1093
|
-
if (pluginEntry.viewerPort
|
|
2048
|
+
if (pluginEntry.viewerPort != null) {
|
|
2049
|
+
result.viewerPort = pluginEntry.viewerPort;
|
|
2050
|
+
}
|
|
2051
|
+
else if (topEntry.viewerPort) {
|
|
1094
2052
|
result.viewerPort = topEntry.viewerPort;
|
|
1095
2053
|
}
|
|
1096
2054
|
this.jsonResponse(res, result);
|
|
@@ -1136,6 +2094,15 @@ class ViewerServer {
|
|
|
1136
2094
|
config.viewerPort = newCfg.viewerPort;
|
|
1137
2095
|
if (newCfg.telemetry !== undefined)
|
|
1138
2096
|
config.telemetry = newCfg.telemetry;
|
|
2097
|
+
if (newCfg.sharing !== undefined) {
|
|
2098
|
+
const existing = config.sharing || {};
|
|
2099
|
+
const merged = { ...existing, ...newCfg.sharing };
|
|
2100
|
+
// Deep-merge capabilities so new keys don't wipe existing ones
|
|
2101
|
+
if (newCfg.sharing.capabilities && existing.capabilities) {
|
|
2102
|
+
merged.capabilities = { ...existing.capabilities, ...newCfg.sharing.capabilities };
|
|
2103
|
+
}
|
|
2104
|
+
config.sharing = merged;
|
|
2105
|
+
}
|
|
1139
2106
|
node_fs_1.default.mkdirSync(node_path_1.default.dirname(cfgPath), { recursive: true });
|
|
1140
2107
|
node_fs_1.default.writeFileSync(cfgPath, JSON.stringify(raw, null, 2), "utf-8");
|
|
1141
2108
|
this.log.info("Plugin config updated via Viewer");
|
|
@@ -1148,6 +2115,89 @@ class ViewerServer {
|
|
|
1148
2115
|
}
|
|
1149
2116
|
});
|
|
1150
2117
|
}
|
|
2118
|
+
handleUpdateUsername(req, res) {
|
|
2119
|
+
this.readBody(req, async (body) => {
|
|
2120
|
+
if (!this.ctx)
|
|
2121
|
+
return this.jsonResponse(res, { error: "sharing_unavailable" });
|
|
2122
|
+
try {
|
|
2123
|
+
const { username } = JSON.parse(body || "{}");
|
|
2124
|
+
if (!username || typeof username !== "string" || username.trim().length < 2 || username.trim().length > 32) {
|
|
2125
|
+
return this.jsonResponse(res, { error: "invalid_username" }, 400);
|
|
2126
|
+
}
|
|
2127
|
+
const trimmed = username.trim();
|
|
2128
|
+
const hubClient = await this.resolveHubClientAware();
|
|
2129
|
+
const result = await (0, hub_1.hubRequestJson)(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/me/update-profile", {
|
|
2130
|
+
method: "POST",
|
|
2131
|
+
body: JSON.stringify({ username: trimmed }),
|
|
2132
|
+
});
|
|
2133
|
+
if (result.ok && result.userToken) {
|
|
2134
|
+
const sharing = this.ctx.config.sharing;
|
|
2135
|
+
if (sharing?.role === "hub") {
|
|
2136
|
+
try {
|
|
2137
|
+
const authPath = node_path_1.default.join(this.dataDir, "hub-auth.json");
|
|
2138
|
+
const authData = JSON.parse(node_fs_1.default.readFileSync(authPath, "utf8"));
|
|
2139
|
+
authData.bootstrapAdminToken = result.userToken;
|
|
2140
|
+
node_fs_1.default.writeFileSync(authPath, JSON.stringify(authData, null, 2), "utf-8");
|
|
2141
|
+
this.log.info("hub-auth.json updated with new admin token after username change");
|
|
2142
|
+
}
|
|
2143
|
+
catch (e) {
|
|
2144
|
+
this.log.warn(`Failed to update hub-auth.json: ${e}`);
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
else {
|
|
2148
|
+
const persisted = this.store.getClientHubConnection();
|
|
2149
|
+
if (persisted) {
|
|
2150
|
+
this.store.setClientHubConnection({
|
|
2151
|
+
...persisted,
|
|
2152
|
+
username: result.username,
|
|
2153
|
+
userToken: result.userToken,
|
|
2154
|
+
});
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
this.jsonResponse(res, result);
|
|
2159
|
+
}
|
|
2160
|
+
catch (err) {
|
|
2161
|
+
const msg = String(err?.message || err);
|
|
2162
|
+
if (msg.includes("409") || msg.includes("username_taken")) {
|
|
2163
|
+
return this.jsonResponse(res, { error: "username_taken" }, 409);
|
|
2164
|
+
}
|
|
2165
|
+
this.jsonResponse(res, { error: msg }, 500);
|
|
2166
|
+
}
|
|
2167
|
+
});
|
|
2168
|
+
}
|
|
2169
|
+
handleTestHubConnection(req, res) {
|
|
2170
|
+
this.readBody(req, async (body) => {
|
|
2171
|
+
try {
|
|
2172
|
+
const { hubUrl } = JSON.parse(body);
|
|
2173
|
+
if (!hubUrl) {
|
|
2174
|
+
this.jsonResponse(res, { ok: false, error: "hubUrl is required" });
|
|
2175
|
+
return;
|
|
2176
|
+
}
|
|
2177
|
+
const url = hubUrl.replace(/\/+$/, "") + "/api/v1/hub/info";
|
|
2178
|
+
const ctrl = new AbortController();
|
|
2179
|
+
const timeout = setTimeout(() => ctrl.abort(), 8000);
|
|
2180
|
+
try {
|
|
2181
|
+
const r = await fetch(url, { signal: ctrl.signal });
|
|
2182
|
+
clearTimeout(timeout);
|
|
2183
|
+
if (!r.ok) {
|
|
2184
|
+
this.jsonResponse(res, { ok: false, error: `HTTP ${r.status}` });
|
|
2185
|
+
return;
|
|
2186
|
+
}
|
|
2187
|
+
const info = await r.json();
|
|
2188
|
+
this.jsonResponse(res, { ok: true, teamName: info.teamName || "", apiVersion: info.apiVersion || "" });
|
|
2189
|
+
}
|
|
2190
|
+
catch (e) {
|
|
2191
|
+
clearTimeout(timeout);
|
|
2192
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2193
|
+
this.jsonResponse(res, { ok: false, error: msg.includes("abort") ? "Connection timeout (8s)" : msg });
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
catch (e) {
|
|
2197
|
+
this.jsonResponse(res, { ok: false, error: String(e) });
|
|
2198
|
+
}
|
|
2199
|
+
});
|
|
2200
|
+
}
|
|
1151
2201
|
handleTestModel(req, res) {
|
|
1152
2202
|
this.readBody(req, async (body) => {
|
|
1153
2203
|
try {
|
|
@@ -1260,36 +2310,133 @@ class ViewerServer {
|
|
|
1260
2310
|
req.on("data", (chunk) => { body += chunk.toString(); });
|
|
1261
2311
|
req.on("end", () => {
|
|
1262
2312
|
try {
|
|
1263
|
-
const { packageSpec } = JSON.parse(body);
|
|
1264
|
-
if (!
|
|
2313
|
+
const { packageSpec: rawSpec } = JSON.parse(body);
|
|
2314
|
+
if (!rawSpec || typeof rawSpec !== "string") {
|
|
1265
2315
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1266
2316
|
res.end(JSON.stringify({ ok: false, error: "Missing packageSpec" }));
|
|
1267
2317
|
return;
|
|
1268
2318
|
}
|
|
2319
|
+
const packageSpec = rawSpec.trim().replace(/^(?:npx\s+)?openclaw\s+plugins\s+install\s+/i, "");
|
|
1269
2320
|
const allowed = /^@[\w-]+\/[\w.-]+(@[\w.-]+)?$/;
|
|
2321
|
+
this.log.info(`update-install: received packageSpec="${packageSpec}" (len=${packageSpec.length})`);
|
|
1270
2322
|
if (!allowed.test(packageSpec)) {
|
|
2323
|
+
this.log.warn(`update-install: rejected packageSpec="${packageSpec}" — does not match ${allowed}`);
|
|
1271
2324
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1272
|
-
res.end(JSON.stringify({ ok: false, error:
|
|
2325
|
+
res.end(JSON.stringify({ ok: false, error: `Invalid package spec: "${packageSpec}"` }));
|
|
1273
2326
|
return;
|
|
1274
2327
|
}
|
|
1275
|
-
this.
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
2328
|
+
const pkgPath = this.findPluginPackageJson();
|
|
2329
|
+
const pluginName = pkgPath
|
|
2330
|
+
? (() => { try {
|
|
2331
|
+
return JSON.parse(node_fs_1.default.readFileSync(pkgPath, "utf-8")).name;
|
|
2332
|
+
}
|
|
2333
|
+
catch {
|
|
2334
|
+
return null;
|
|
2335
|
+
} })()
|
|
2336
|
+
: null;
|
|
2337
|
+
const shortName = pluginName?.replace(/^@[\w-]+\//, "") ?? "memos-local-openclaw-plugin";
|
|
2338
|
+
const extDir = node_path_1.default.join(node_os_1.default.homedir(), ".openclaw", "extensions", shortName);
|
|
2339
|
+
const tmpDir = node_path_1.default.join(node_os_1.default.tmpdir(), `openclaw-update-${Date.now()}`);
|
|
2340
|
+
// Download via npm pack, extract, and replace extension dir.
|
|
2341
|
+
// Does NOT touch openclaw.json → no config watcher SIGUSR1.
|
|
2342
|
+
this.log.info(`update-install: downloading ${packageSpec} via npm pack...`);
|
|
2343
|
+
node_fs_1.default.mkdirSync(tmpDir, { recursive: true });
|
|
2344
|
+
(0, node_child_process_1.exec)(`npm pack ${packageSpec} --pack-destination ${tmpDir}`, { timeout: 60_000 }, (packErr, packOut) => {
|
|
2345
|
+
if (packErr) {
|
|
2346
|
+
this.log.warn(`update-install: npm pack failed: ${packErr.message}`);
|
|
2347
|
+
this.jsonResponse(res, { ok: false, error: `Download failed: ${packErr.message}` });
|
|
2348
|
+
try {
|
|
2349
|
+
node_fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
|
|
2350
|
+
}
|
|
2351
|
+
catch { }
|
|
1280
2352
|
return;
|
|
1281
2353
|
}
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
this.log.info(`update-install:
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
2354
|
+
const tgzFile = packOut.trim().split("\n").pop();
|
|
2355
|
+
const tgzPath = node_path_1.default.join(tmpDir, tgzFile);
|
|
2356
|
+
this.log.info(`update-install: downloaded ${tgzFile}, extracting...`);
|
|
2357
|
+
const extractDir = node_path_1.default.join(tmpDir, "extract");
|
|
2358
|
+
node_fs_1.default.mkdirSync(extractDir, { recursive: true });
|
|
2359
|
+
(0, node_child_process_1.exec)(`tar -xzf ${tgzPath} -C ${extractDir}`, { timeout: 30_000 }, (tarErr) => {
|
|
2360
|
+
if (tarErr) {
|
|
2361
|
+
this.log.warn(`update-install: tar extract failed: ${tarErr.message}`);
|
|
2362
|
+
this.jsonResponse(res, { ok: false, error: `Extract failed: ${tarErr.message}` });
|
|
2363
|
+
try {
|
|
2364
|
+
node_fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
|
|
2365
|
+
}
|
|
2366
|
+
catch { }
|
|
2367
|
+
return;
|
|
2368
|
+
}
|
|
2369
|
+
// npm pack extracts to a "package" subdirectory
|
|
2370
|
+
const srcDir = node_path_1.default.join(extractDir, "package");
|
|
2371
|
+
if (!node_fs_1.default.existsSync(srcDir)) {
|
|
2372
|
+
this.jsonResponse(res, { ok: false, error: "Extracted package has no 'package' dir" });
|
|
2373
|
+
try {
|
|
2374
|
+
node_fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
|
|
2375
|
+
}
|
|
2376
|
+
catch { }
|
|
2377
|
+
return;
|
|
2378
|
+
}
|
|
2379
|
+
// Replace extension directory
|
|
2380
|
+
this.log.info(`update-install: replacing ${extDir}...`);
|
|
2381
|
+
try {
|
|
2382
|
+
node_fs_1.default.rmSync(extDir, { recursive: true, force: true });
|
|
2383
|
+
}
|
|
2384
|
+
catch { }
|
|
2385
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(extDir), { recursive: true });
|
|
2386
|
+
node_fs_1.default.renameSync(srcDir, extDir);
|
|
2387
|
+
// Install dependencies
|
|
2388
|
+
this.log.info(`update-install: installing dependencies...`);
|
|
2389
|
+
(0, node_child_process_1.exec)(`cd ${extDir} && npm install --omit=dev --ignore-scripts`, { timeout: 120_000 }, (npmErr, npmOut, npmStderr) => {
|
|
2390
|
+
if (npmErr) {
|
|
2391
|
+
try {
|
|
2392
|
+
node_fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
|
|
2393
|
+
}
|
|
2394
|
+
catch { }
|
|
2395
|
+
this.log.warn(`update-install: npm install failed: ${npmErr.message}`);
|
|
2396
|
+
this.jsonResponse(res, { ok: false, error: `Dependency install failed: ${npmStderr || npmErr.message}` });
|
|
2397
|
+
return;
|
|
2398
|
+
}
|
|
2399
|
+
// Rebuild native modules (do not swallow errors)
|
|
2400
|
+
(0, node_child_process_1.exec)(`cd ${extDir} && npm rebuild better-sqlite3`, { timeout: 60_000 }, (rebuildErr, rebuildOut, rebuildStderr) => {
|
|
2401
|
+
if (rebuildErr) {
|
|
2402
|
+
this.log.warn(`update-install: better-sqlite3 rebuild failed: ${rebuildErr.message}`);
|
|
2403
|
+
const stderr = String(rebuildStderr || "").trim();
|
|
2404
|
+
if (stderr)
|
|
2405
|
+
this.log.warn(`update-install: rebuild stderr: ${stderr.slice(0, 500)}`);
|
|
2406
|
+
// Continue so postinstall.cjs can run (it will try rebuild again and show user guidance)
|
|
2407
|
+
}
|
|
2408
|
+
// Run postinstall.cjs: legacy cleanup, skill install, version marker, and optional sqlite re-check
|
|
2409
|
+
this.log.info(`update-install: running postinstall...`);
|
|
2410
|
+
(0, node_child_process_1.exec)(`cd ${extDir} && node scripts/postinstall.cjs`, { timeout: 180_000 }, (postErr, postOut, postStderr) => {
|
|
2411
|
+
try {
|
|
2412
|
+
node_fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
|
|
2413
|
+
}
|
|
2414
|
+
catch { }
|
|
2415
|
+
if (postErr) {
|
|
2416
|
+
this.log.warn(`update-install: postinstall failed: ${postErr.message}`);
|
|
2417
|
+
const postStderrStr = String(postStderr || "").trim();
|
|
2418
|
+
if (postStderrStr)
|
|
2419
|
+
this.log.warn(`update-install: postinstall stderr: ${postStderrStr.slice(0, 500)}`);
|
|
2420
|
+
// Still report success; plugin is updated, user can run postinstall manually if needed
|
|
2421
|
+
}
|
|
2422
|
+
// Read new version
|
|
2423
|
+
let newVersion = "unknown";
|
|
2424
|
+
try {
|
|
2425
|
+
const newPkg = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(extDir, "package.json"), "utf-8"));
|
|
2426
|
+
newVersion = newPkg.version ?? newVersion;
|
|
2427
|
+
}
|
|
2428
|
+
catch { }
|
|
2429
|
+
this.log.info(`update-install: success! Updated to ${newVersion}`);
|
|
2430
|
+
this.jsonResponse(res, { ok: true, version: newVersion });
|
|
2431
|
+
// Trigger Gateway restart after response is sent
|
|
2432
|
+
setTimeout(() => {
|
|
2433
|
+
this.log.info(`update-install: triggering gateway restart...`);
|
|
2434
|
+
process.kill(process.pid, "SIGUSR1");
|
|
2435
|
+
}, 500);
|
|
2436
|
+
});
|
|
2437
|
+
});
|
|
1291
2438
|
});
|
|
1292
|
-
}
|
|
2439
|
+
});
|
|
1293
2440
|
});
|
|
1294
2441
|
}
|
|
1295
2442
|
catch (e) {
|
|
@@ -2362,8 +3509,8 @@ class ViewerServer {
|
|
|
2362
3509
|
req.on("data", (chunk) => { body += chunk.toString(); });
|
|
2363
3510
|
req.on("end", () => cb(body));
|
|
2364
3511
|
}
|
|
2365
|
-
jsonResponse(res, data) {
|
|
2366
|
-
res.writeHead(
|
|
3512
|
+
jsonResponse(res, data, statusCode = 200) {
|
|
3513
|
+
res.writeHead(statusCode, { "Content-Type": "application/json; charset=utf-8" });
|
|
2367
3514
|
res.end(JSON.stringify(data));
|
|
2368
3515
|
}
|
|
2369
3516
|
}
|