@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.
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 +21 -4
  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 +242 -8
  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 +2 -2
  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/update-check.d.ts.map +1 -1
  96. package/dist/update-check.js +0 -1
  97. package/dist/update-check.js.map +1 -1
  98. package/dist/viewer/html.d.ts.map +1 -1
  99. package/dist/viewer/html.js +2396 -289
  100. package/dist/viewer/html.js.map +1 -1
  101. package/dist/viewer/server.d.ts +43 -0
  102. package/dist/viewer/server.d.ts.map +1 -1
  103. package/dist/viewer/server.js +1180 -33
  104. package/dist/viewer/server.js.map +1 -1
  105. package/index.ts +445 -25
  106. package/openclaw.plugin.json +2 -1
  107. package/package.json +2 -1
  108. package/scripts/postinstall.cjs +282 -45
  109. package/skill/memos-memory-guide/SKILL.md +26 -2
  110. package/src/client/connector.ts +124 -0
  111. package/src/client/hub.ts +189 -0
  112. package/src/client/skill-sync.ts +202 -0
  113. package/src/config.ts +92 -3
  114. package/src/embedding/index.ts +25 -3
  115. package/src/hub/auth.ts +78 -0
  116. package/src/hub/server.ts +734 -0
  117. package/src/hub/user-manager.ts +126 -0
  118. package/src/index.ts +7 -4
  119. package/src/ingest/providers/index.ts +279 -8
  120. package/src/ingest/providers/openai.ts +1 -1
  121. package/src/ingest/task-processor.ts +1 -1
  122. package/src/openclaw-api.ts +287 -0
  123. package/src/recall/engine.ts +2 -2
  124. package/src/shared/llm-call.ts +19 -1
  125. package/src/sharing/types.contract.ts +40 -0
  126. package/src/sharing/types.ts +102 -0
  127. package/src/skill/evaluator.ts +3 -2
  128. package/src/skill/generator.ts +6 -4
  129. package/src/skill/upgrader.ts +1 -1
  130. package/src/skill/validator.ts +1 -1
  131. package/src/storage/sqlite.ts +1167 -7
  132. package/src/tools/index.ts +1 -0
  133. package/src/tools/memory-search.ts +57 -8
  134. package/src/tools/network-memory-detail.ts +34 -0
  135. package/src/types.ts +48 -2
  136. package/src/update-check.ts +0 -1
  137. package/src/viewer/html.ts +2396 -289
  138. package/src/viewer/server.ts +1087 -34
@@ -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 memories = rawMemories.map((m) => {
458
- if (m.role === "user" && m.content) {
459
- 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 {
460
540
  }
461
- 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) {
462
545
  const sources = findMergeSources.all(m.id);
463
- m.merge_sources = sources;
546
+ out.merge_sources = sources;
464
547
  }
465
- 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;
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 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());
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
- 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 });
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 == null && topEntry.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 (!packageSpec || typeof packageSpec !== "string") {
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: "Invalid package spec" }));
2325
+ res.end(JSON.stringify({ ok: false, error: `Invalid package spec: "${packageSpec}"` }));
1273
2326
  return;
1274
2327
  }
1275
- this.log.info(`update-install: installing ${packageSpec}...`);
1276
- (0, node_child_process_1.exec)(`npx openclaw plugins install ${packageSpec}`, { timeout: 120_000 }, (err, stdout, stderr) => {
1277
- if (err) {
1278
- this.log.warn(`update-install failed: ${err.message}\n${stderr}`);
1279
- this.jsonResponse(res, { ok: false, error: stderr || err.message });
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
- this.log.info(`update-install success: ${stdout}`);
1283
- this.jsonResponse(res, { ok: true, output: stdout });
1284
- this.log.info(`update-install: restarting gateway...`);
1285
- setTimeout(() => {
1286
- (0, node_child_process_1.exec)("npx openclaw gateway restart", { timeout: 30_000 }, (restartErr) => {
1287
- if (restartErr)
1288
- this.log.warn(`gateway restart failed: ${restartErr.message}`);
1289
- else
1290
- this.log.info("gateway restart initiated");
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
- }, 1000);
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(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" });
2367
3514
  res.end(JSON.stringify(data));
2368
3515
  }
2369
3516
  }