@memtensor/memos-local-openclaw-plugin 1.0.3 → 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.
Files changed (138) hide show
  1. package/README.md +38 -21
  2. package/dist/client/connector.d.ts +26 -0
  3. package/dist/client/connector.d.ts.map +1 -0
  4. package/dist/client/connector.js +127 -0
  5. package/dist/client/connector.js.map +1 -0
  6. package/dist/client/hub.d.ts +61 -0
  7. package/dist/client/hub.d.ts.map +1 -0
  8. package/dist/client/hub.js +148 -0
  9. package/dist/client/hub.js.map +1 -0
  10. package/dist/client/skill-sync.d.ts +29 -0
  11. package/dist/client/skill-sync.d.ts.map +1 -0
  12. package/dist/client/skill-sync.js +216 -0
  13. package/dist/client/skill-sync.js.map +1 -0
  14. package/dist/config.d.ts +2 -1
  15. package/dist/config.d.ts.map +1 -1
  16. package/dist/config.js +70 -3
  17. package/dist/config.js.map +1 -1
  18. package/dist/embedding/index.d.ts +4 -2
  19. package/dist/embedding/index.d.ts.map +1 -1
  20. package/dist/embedding/index.js +17 -1
  21. package/dist/embedding/index.js.map +1 -1
  22. package/dist/hub/auth.d.ts +19 -0
  23. package/dist/hub/auth.d.ts.map +1 -0
  24. package/dist/hub/auth.js +70 -0
  25. package/dist/hub/auth.js.map +1 -0
  26. package/dist/hub/server.d.ts +41 -0
  27. package/dist/hub/server.d.ts.map +1 -0
  28. package/dist/hub/server.js +742 -0
  29. package/dist/hub/server.js.map +1 -0
  30. package/dist/hub/user-manager.d.ts +28 -0
  31. package/dist/hub/user-manager.d.ts.map +1 -0
  32. package/dist/hub/user-manager.js +112 -0
  33. package/dist/hub/user-manager.js.map +1 -0
  34. package/dist/index.d.ts +2 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +4 -3
  37. package/dist/index.js.map +1 -1
  38. package/dist/ingest/providers/index.d.ts +10 -2
  39. package/dist/ingest/providers/index.d.ts.map +1 -1
  40. package/dist/ingest/providers/index.js +203 -6
  41. package/dist/ingest/providers/index.js.map +1 -1
  42. package/dist/ingest/providers/openai.d.ts +1 -0
  43. package/dist/ingest/providers/openai.d.ts.map +1 -1
  44. package/dist/ingest/providers/openai.js +1 -0
  45. package/dist/ingest/providers/openai.js.map +1 -1
  46. package/dist/ingest/task-processor.js +1 -1
  47. package/dist/ingest/task-processor.js.map +1 -1
  48. package/dist/openclaw-api.d.ts +53 -0
  49. package/dist/openclaw-api.d.ts.map +1 -0
  50. package/dist/openclaw-api.js +189 -0
  51. package/dist/openclaw-api.js.map +1 -0
  52. package/dist/recall/engine.js +1 -1
  53. package/dist/recall/engine.js.map +1 -1
  54. package/dist/shared/llm-call.d.ts +4 -1
  55. package/dist/shared/llm-call.d.ts.map +1 -1
  56. package/dist/shared/llm-call.js +15 -0
  57. package/dist/shared/llm-call.js.map +1 -1
  58. package/dist/sharing/types.contract.d.ts +2 -0
  59. package/dist/sharing/types.contract.d.ts.map +1 -0
  60. package/dist/sharing/types.contract.js +3 -0
  61. package/dist/sharing/types.contract.js.map +1 -0
  62. package/dist/sharing/types.d.ts +80 -0
  63. package/dist/sharing/types.d.ts.map +1 -0
  64. package/dist/sharing/types.js +3 -0
  65. package/dist/sharing/types.js.map +1 -0
  66. package/dist/skill/evaluator.d.ts.map +1 -1
  67. package/dist/skill/evaluator.js +2 -2
  68. package/dist/skill/evaluator.js.map +1 -1
  69. package/dist/skill/generator.d.ts.map +1 -1
  70. package/dist/skill/generator.js +4 -4
  71. package/dist/skill/generator.js.map +1 -1
  72. package/dist/skill/upgrader.js +1 -1
  73. package/dist/skill/upgrader.js.map +1 -1
  74. package/dist/skill/validator.js +1 -1
  75. package/dist/skill/validator.js.map +1 -1
  76. package/dist/storage/sqlite.d.ts +294 -0
  77. package/dist/storage/sqlite.d.ts.map +1 -1
  78. package/dist/storage/sqlite.js +902 -8
  79. package/dist/storage/sqlite.js.map +1 -1
  80. package/dist/tools/index.d.ts +1 -0
  81. package/dist/tools/index.d.ts.map +1 -1
  82. package/dist/tools/index.js +3 -1
  83. package/dist/tools/index.js.map +1 -1
  84. package/dist/tools/memory-search.d.ts +3 -2
  85. package/dist/tools/memory-search.d.ts.map +1 -1
  86. package/dist/tools/memory-search.js +48 -7
  87. package/dist/tools/memory-search.js.map +1 -1
  88. package/dist/tools/network-memory-detail.d.ts +4 -0
  89. package/dist/tools/network-memory-detail.d.ts.map +1 -0
  90. package/dist/tools/network-memory-detail.js +34 -0
  91. package/dist/tools/network-memory-detail.js.map +1 -0
  92. package/dist/types.d.ts +47 -2
  93. package/dist/types.d.ts.map +1 -1
  94. package/dist/types.js.map +1 -1
  95. package/dist/viewer/html.d.ts.map +1 -1
  96. package/dist/viewer/html.js +2323 -252
  97. package/dist/viewer/html.js.map +1 -1
  98. package/dist/viewer/server.d.ts +43 -0
  99. package/dist/viewer/server.d.ts.map +1 -1
  100. package/dist/viewer/server.js +1064 -15
  101. package/dist/viewer/server.js.map +1 -1
  102. package/index.ts +428 -16
  103. package/openclaw.plugin.json +2 -1
  104. package/package.json +3 -3
  105. package/scripts/postinstall.cjs +282 -45
  106. package/skill/memos-memory-guide/SKILL.md +26 -2
  107. package/src/client/connector.ts +124 -0
  108. package/src/client/hub.ts +189 -0
  109. package/src/client/skill-sync.ts +202 -0
  110. package/src/config.ts +92 -3
  111. package/src/embedding/index.ts +21 -1
  112. package/src/hub/auth.ts +78 -0
  113. package/src/hub/server.ts +734 -0
  114. package/src/hub/user-manager.ts +126 -0
  115. package/src/index.ts +7 -4
  116. package/src/ingest/providers/index.ts +240 -6
  117. package/src/ingest/providers/openai.ts +1 -1
  118. package/src/ingest/task-processor.ts +1 -1
  119. package/src/openclaw-api.ts +287 -0
  120. package/src/recall/engine.ts +1 -1
  121. package/src/shared/llm-call.ts +19 -1
  122. package/src/sharing/types.contract.ts +40 -0
  123. package/src/sharing/types.ts +102 -0
  124. package/src/skill/evaluator.ts +3 -2
  125. package/src/skill/generator.ts +6 -4
  126. package/src/skill/upgrader.ts +1 -1
  127. package/src/skill/validator.ts +1 -1
  128. package/src/storage/sqlite.ts +1167 -7
  129. package/src/tools/index.ts +1 -0
  130. package/src/tools/memory-search.ts +57 -8
  131. package/src/tools/network-memory-detail.ts +34 -0
  132. package/src/types.ts +42 -2
  133. package/src/viewer/html.ts +2323 -252
  134. package/src/viewer/server.ts +985 -18
  135. package/dist/ingest/extra-paths.d.ts +0 -13
  136. package/dist/ingest/extra-paths.d.ts.map +0 -1
  137. package/dist/ingest/extra-paths.js +0 -173
  138. package/dist/ingest/extra-paths.js.map +0 -1
@@ -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) {
@@ -225,7 +229,7 @@ class ViewerServer {
225
229
  if (p === "/api/memories" && req.method === "GET")
226
230
  this.serveMemories(res, url);
227
231
  else if (p === "/api/stats")
228
- this.serveStats(res);
232
+ this.serveStats(res, url);
229
233
  else if (p === "/api/metrics")
230
234
  this.serveMetrics(res, url);
231
235
  else if (p === "/api/tool-metrics")
@@ -270,6 +274,74 @@ class ViewerServer {
270
274
  this.serveLogs(res, url);
271
275
  else if (p === "/api/log-tools" && req.method === "GET")
272
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);
273
345
  else if (p === "/api/config" && req.method === "GET")
274
346
  this.serveConfig(res);
275
347
  else if (p === "/api/config" && req.method === "PUT")
@@ -455,15 +527,28 @@ class ViewerServer {
455
527
  const totalRow = db.prepare("SELECT COUNT(*) as count FROM chunks" + where).get(...params);
456
528
  const rawMemories = db.prepare("SELECT * FROM chunks" + where + ` ORDER BY created_at ${sortBy} LIMIT ? OFFSET ?`).all(...params, limit, offset);
457
529
  const findMergeSources = db.prepare("SELECT id, summary, role FROM chunks WHERE dedup_target = ? AND (dedup_status = 'merged' OR dedup_status = 'duplicate')");
458
- const memories = rawMemories.map((m) => {
459
- if (m.role === "user" && m.content) {
460
- m = { ...m, content: (0, capture_1.stripInboundMetadata)(m.content) };
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 {
461
540
  }
462
- if (m.merge_count > 0) {
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) {
463
545
  const sources = findMergeSources.all(m.id);
464
- m.merge_sources = sources;
546
+ out.merge_sources = sources;
465
547
  }
466
- return m;
548
+ const shared = sharingMap.get(m.id);
549
+ out.sharingVisibility = shared?.visibility ?? null;
550
+ out.sharingGroupId = shared?.group_id ?? null;
551
+ return out;
467
552
  });
468
553
  this.store.recordViewerEvent("list");
469
554
  this.jsonResponse(res, {
@@ -528,6 +613,7 @@ class ViewerServer {
528
613
  }));
529
614
  const db = this.store.db;
530
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);
531
617
  this.jsonResponse(res, {
532
618
  id: task.id,
533
619
  sessionKey: task.sessionKey,
@@ -540,9 +626,11 @@ class ViewerServer {
540
626
  skillStatus: meta?.skill_status ?? null,
541
627
  skillReason: meta?.skill_reason ?? null,
542
628
  skillLinks,
629
+ sharingVisibility: sharedTask?.visibility ?? null,
630
+ sharingGroupId: sharedTask?.group_id ?? null,
543
631
  });
544
632
  }
545
- serveStats(res) {
633
+ serveStats(res, url) {
546
634
  const emptyStats = {
547
635
  totalMemories: 0, totalSessions: 0, totalEmbeddings: 0, totalSkills: 0,
548
636
  embeddingProvider: this.embedder?.provider ?? "none",
@@ -554,6 +642,7 @@ class ViewerServer {
554
642
  this.jsonResponse(res, emptyStats);
555
643
  return;
556
644
  }
645
+ const ownerFilter = url?.searchParams.get("owner") ?? "";
557
646
  try {
558
647
  const db = this.store.db;
559
648
  const total = db.prepare("SELECT COUNT(*) as count FROM chunks").get();
@@ -572,7 +661,12 @@ class ViewerServer {
572
661
  embCount = db.prepare("SELECT COUNT(*) as count FROM embeddings").get().count;
573
662
  }
574
663
  catch { /* table may not exist */ }
575
- const sessionList = db.prepare("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").all();
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());
576
670
  let skillCount = 0;
577
671
  try {
578
672
  skillCount = db.prepare("SELECT COUNT(*) as count FROM skills").get().count;
@@ -734,8 +828,10 @@ class ViewerServer {
734
828
  const versions = this.store.getSkillVersions(skillId);
735
829
  const relatedTasks = this.store.getTasksBySkill(skillId);
736
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);
737
833
  this.jsonResponse(res, {
738
- skill,
834
+ skill: { ...skill, sharingVisibility: sharedSkill?.visibility ?? null, sharingGroupId: sharedSkill?.group_id ?? null },
739
835
  versions: versions.map(v => ({
740
836
  id: v.id,
741
837
  version: v.version,
@@ -844,7 +940,7 @@ class ViewerServer {
844
940
  handleSkillVisibility(req, res, urlPath) {
845
941
  const segments = urlPath.split("/");
846
942
  const skillId = segments[segments.length - 2];
847
- this.readBody(req, (body) => {
943
+ this.readBody(req, async (body) => {
848
944
  try {
849
945
  const parsed = JSON.parse(body);
850
946
  const visibility = parsed.visibility;
@@ -860,7 +956,47 @@ class ViewerServer {
860
956
  return;
861
957
  }
862
958
  this.store.setSkillVisibility(skillId, visibility);
863
- this.jsonResponse(res, { ok: true, skillId, visibility });
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 });
864
1000
  }
865
1001
  catch (err) {
866
1002
  const errMsg = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
@@ -1071,6 +1207,824 @@ class ViewerServer {
1071
1207
  const home = process.env.HOME || process.env.USERPROFILE || "";
1072
1208
  return node_path_1.default.join(home, ".openclaw", "openclaw.json");
1073
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
+ }
1074
2028
  serveConfig(res) {
1075
2029
  try {
1076
2030
  const cfgPath = this.getOpenClawConfigPath();
@@ -1091,7 +2045,10 @@ class ViewerServer {
1091
2045
  ?? entries["memos-lite-openclaw-plugin"]
1092
2046
  ?? entries["memos-lite"]
1093
2047
  ?? {};
1094
- if (pluginEntry.viewerPort == null && topEntry.viewerPort) {
2048
+ if (pluginEntry.viewerPort != null) {
2049
+ result.viewerPort = pluginEntry.viewerPort;
2050
+ }
2051
+ else if (topEntry.viewerPort) {
1095
2052
  result.viewerPort = topEntry.viewerPort;
1096
2053
  }
1097
2054
  this.jsonResponse(res, result);
@@ -1137,6 +2094,15 @@ class ViewerServer {
1137
2094
  config.viewerPort = newCfg.viewerPort;
1138
2095
  if (newCfg.telemetry !== undefined)
1139
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
+ }
1140
2106
  node_fs_1.default.mkdirSync(node_path_1.default.dirname(cfgPath), { recursive: true });
1141
2107
  node_fs_1.default.writeFileSync(cfgPath, JSON.stringify(raw, null, 2), "utf-8");
1142
2108
  this.log.info("Plugin config updated via Viewer");
@@ -1149,6 +2115,89 @@ class ViewerServer {
1149
2115
  }
1150
2116
  });
1151
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
+ }
1152
2201
  handleTestModel(req, res) {
1153
2202
  this.readBody(req, async (body) => {
1154
2203
  try {
@@ -2460,8 +3509,8 @@ class ViewerServer {
2460
3509
  req.on("data", (chunk) => { body += chunk.toString(); });
2461
3510
  req.on("end", () => cb(body));
2462
3511
  }
2463
- jsonResponse(res, data) {
2464
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
3512
+ jsonResponse(res, data, statusCode = 200) {
3513
+ res.writeHead(statusCode, { "Content-Type": "application/json; charset=utf-8" });
2465
3514
  res.end(JSON.stringify(data));
2466
3515
  }
2467
3516
  }