@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
@@ -1,4 +1,5 @@
1
1
  import http from "node:http";
2
+ import os from "node:os";
2
3
  import crypto from "node:crypto";
3
4
  import { execSync, exec } from "node:child_process";
4
5
  import fs from "node:fs";
@@ -13,7 +14,11 @@ import { vectorSearch } from "../storage/vector";
13
14
  import { TaskProcessor } from "../ingest/task-processor";
14
15
  import { RecallEngine } from "../recall/engine";
15
16
  import { SkillEvolver } from "../skill/evolver";
16
- import type { Logger, Chunk, PluginContext } from "../types";
17
+ import { resolveConfig } from "../config";
18
+ import { getHubStatus } from "../client/connector";
19
+ import { type ResolvedHubClient, hubGetMemoryDetail, hubListMemories, hubListTasks, hubListSkills, hubRequestJson, hubSearchMemories, hubSearchSkills, hubUpdateUsername, normalizeHubUrl, resolveHubClient } from "../client/hub";
20
+ import { buildSkillBundleForHub, fetchHubSkillBundle, restoreSkillBundleFromHub } from "../client/skill-sync";
21
+ import type { Logger, Chunk, PluginContext, MemosLocalConfig } from "../types";
17
22
  import { viewerHTML } from "./html";
18
23
  import { v4 as uuid } from "uuid";
19
24
 
@@ -219,7 +224,7 @@ export class ViewerServer {
219
224
  }
220
225
 
221
226
  if (p === "/api/memories" && req.method === "GET") this.serveMemories(res, url);
222
- else if (p === "/api/stats") this.serveStats(res);
227
+ else if (p === "/api/stats") this.serveStats(res, url);
223
228
  else if (p === "/api/metrics") this.serveMetrics(res, url);
224
229
  else if (p === "/api/tool-metrics") this.serveToolMetrics(res, url);
225
230
  else if (p === "/api/search") this.serveSearch(req, res, url);
@@ -242,6 +247,40 @@ export class ViewerServer {
242
247
  else if (p === "/api/memories" && req.method === "DELETE") this.handleDeleteAll(res);
243
248
  else if (p === "/api/logs" && req.method === "GET") this.serveLogs(res, url);
244
249
  else if (p === "/api/log-tools" && req.method === "GET") this.serveLogTools(res);
250
+ else if (p === "/api/sharing/status" && req.method === "GET") this.serveSharingStatus(res);
251
+ else if (p === "/api/sharing/pending-users" && req.method === "GET") this.serveSharingPendingUsers(res);
252
+ else if (p === "/api/sharing/approve-user" && req.method === "POST") this.handleSharingApproveUser(req, res);
253
+ else if (p === "/api/sharing/reject-user" && req.method === "POST") this.handleSharingRejectUser(req, res);
254
+ else if (p === "/api/sharing/search/memories" && req.method === "POST") this.handleSharingMemorySearch(req, res);
255
+ else if (p === "/api/sharing/memories/list" && req.method === "GET") this.serveSharingMemoryList(res, url);
256
+ else if (p === "/api/sharing/tasks/list" && req.method === "GET") this.serveSharingTaskList(res, url);
257
+ else if (p === "/api/sharing/skills/list" && req.method === "GET") this.serveSharingSkillList(res, url);
258
+ else if (p === "/api/sharing/memory-detail" && req.method === "POST") this.handleSharingMemoryDetail(req, res);
259
+ else if (p === "/api/sharing/search/skills" && req.method === "GET") this.serveSharingSkillSearch(res, url);
260
+ else if (p === "/api/sharing/tasks/share" && req.method === "POST") this.handleSharingTaskShare(req, res);
261
+ else if (p === "/api/sharing/tasks/unshare" && req.method === "POST") this.handleSharingTaskUnshare(req, res);
262
+ else if (p === "/api/sharing/update-username" && req.method === "POST") this.handleUpdateUsername(req, res);
263
+ else if (p === "/api/sharing/test-hub" && req.method === "POST") this.handleTestHubConnection(req, res);
264
+ else if (p === "/api/sharing/memories/share" && req.method === "POST") this.handleSharingMemoryShare(req, res);
265
+ else if (p === "/api/sharing/memories/unshare" && req.method === "POST") this.handleSharingMemoryUnshare(req, res);
266
+ else if (p === "/api/sharing/skills/pull" && req.method === "POST") this.handleSharingSkillPull(req, res);
267
+ else if (p === "/api/sharing/skills/share" && req.method === "POST") this.handleSharingSkillShare(req, res);
268
+ else if (p === "/api/sharing/skills/unshare" && req.method === "POST") this.handleSharingSkillUnshare(req, res);
269
+ else if (p === "/api/sharing/groups" && req.method === "GET") this.serveSharingGroups(res);
270
+ else if (p === "/api/sharing/groups" && req.method === "POST") this.handleSharingGroupCreate(req, res);
271
+ else if (p.match(/^\/api\/sharing\/groups\/[^/]+$/) && req.method === "PUT") this.handleSharingGroupUpdate(req, res, p);
272
+ else if (p.match(/^\/api\/sharing\/groups\/[^/]+$/) && req.method === "DELETE") this.handleSharingGroupDelete(res, p);
273
+ else if (p.match(/^\/api\/sharing\/groups\/[^/]+\/members$/) && req.method === "GET") this.serveSharingGroupMembers(res, p);
274
+ else if (p.match(/^\/api\/sharing\/groups\/[^/]+\/members$/) && req.method === "POST") this.handleSharingGroupAddMember(req, res, p);
275
+ else if (p.match(/^\/api\/sharing\/groups\/[^/]+\/members$/) && req.method === "DELETE") this.handleSharingGroupRemoveMember(req, res, p);
276
+ else if (p === "/api/sharing/users" && req.method === "GET") this.serveSharingUsers(res);
277
+ else if (p === "/api/admin/shared-tasks" && req.method === "GET") this.serveAdminSharedTasks(res);
278
+ else if (p.match(/^\/api\/admin\/shared-tasks\/[^/]+$/) && req.method === "DELETE") this.handleAdminDeleteTask(res, p);
279
+ else if (p === "/api/admin/shared-skills" && req.method === "GET") this.serveAdminSharedSkills(res);
280
+ else if (p.match(/^\/api\/admin\/shared-skills\/[^/]+$/) && req.method === "DELETE") this.handleAdminDeleteSkill(res, p);
281
+ else if (p === "/api/admin/shared-memories" && req.method === "GET") this.serveAdminSharedMemories(res);
282
+ else if (p.match(/^\/api\/admin\/shared-memories\/[^/]+$/) && req.method === "DELETE") this.handleAdminDeleteMemory(res, p);
283
+ else if (p === "/api/local-ips" && req.method === "GET") this.serveLocalIPs(res);
245
284
  else if (p === "/api/config" && req.method === "GET") this.serveConfig(res);
246
285
  else if (p === "/api/config" && req.method === "PUT") this.handleSaveConfig(req, res);
247
286
  else if (p === "/api/test-model" && req.method === "POST") this.handleTestModel(req, res);
@@ -400,15 +439,27 @@ export class ViewerServer {
400
439
  const totalRow = db.prepare("SELECT COUNT(*) as count FROM chunks" + where).get(...params) as any;
401
440
  const rawMemories = db.prepare("SELECT * FROM chunks" + where + ` ORDER BY created_at ${sortBy} LIMIT ? OFFSET ?`).all(...params, limit, offset);
402
441
  const findMergeSources = db.prepare("SELECT id, summary, role FROM chunks WHERE dedup_target = ? AND (dedup_status = 'merged' OR dedup_status = 'duplicate')");
403
- const memories = rawMemories.map((m: any) => {
404
- if (m.role === "user" && m.content) {
405
- m = { ...m, content: stripInboundMetadata(m.content) };
442
+
443
+ const chunkIds = rawMemories.map((m: any) => m.id);
444
+ const sharingMap = new Map<string, { visibility: string; group_id: string | null }>();
445
+ if (chunkIds.length > 0) {
446
+ try {
447
+ const placeholders = chunkIds.map(() => "?").join(",");
448
+ const sharedRows = db.prepare(`SELECT source_chunk_id, visibility, group_id FROM hub_memories WHERE source_chunk_id IN (${placeholders})`).all(...chunkIds) as Array<{ source_chunk_id: string; visibility: string; group_id: string | null }>;
449
+ for (const r of sharedRows) sharingMap.set(r.source_chunk_id, r);
450
+ } catch {
406
451
  }
407
- if (m.merge_count > 0) {
452
+ }
453
+ const memories = rawMemories.map((m: any) => {
454
+ const out: any = m.role === "user" && m.content ? { ...m, content: stripInboundMetadata(m.content) } : { ...m };
455
+ if (out.merge_count > 0) {
408
456
  const sources = findMergeSources.all(m.id) as Array<{ id: string; summary: string; role: string }>;
409
- m.merge_sources = sources;
457
+ out.merge_sources = sources;
410
458
  }
411
- return m;
459
+ const shared = sharingMap.get(m.id);
460
+ out.sharingVisibility = shared?.visibility ?? null;
461
+ out.sharingGroupId = shared?.group_id ?? null;
462
+ return out;
412
463
  });
413
464
 
414
465
  this.store.recordViewerEvent("list");
@@ -484,6 +535,7 @@ export class ViewerServer {
484
535
  const db = (this.store as any).db;
485
536
  const meta = db.prepare("SELECT skill_status, skill_reason FROM tasks WHERE id = ?").get(taskId) as
486
537
  { skill_status: string | null; skill_reason: string | null } | undefined;
538
+ const sharedTask = db.prepare("SELECT visibility, group_id FROM hub_tasks WHERE source_task_id = ? ORDER BY updated_at DESC LIMIT 1").get(taskId) as { visibility: string | null; group_id: string | null } | undefined;
487
539
 
488
540
  this.jsonResponse(res, {
489
541
  id: task.id,
@@ -497,10 +549,12 @@ export class ViewerServer {
497
549
  skillStatus: meta?.skill_status ?? null,
498
550
  skillReason: meta?.skill_reason ?? null,
499
551
  skillLinks,
552
+ sharingVisibility: sharedTask?.visibility ?? null,
553
+ sharingGroupId: sharedTask?.group_id ?? null,
500
554
  });
501
555
  }
502
556
 
503
- private serveStats(res: http.ServerResponse): void {
557
+ private serveStats(res: http.ServerResponse, url?: URL): void {
504
558
  const emptyStats = {
505
559
  totalMemories: 0, totalSessions: 0, totalEmbeddings: 0, totalSkills: 0,
506
560
  embeddingProvider: this.embedder?.provider ?? "none",
@@ -514,6 +568,8 @@ export class ViewerServer {
514
568
  return;
515
569
  }
516
570
 
571
+ const ownerFilter = url?.searchParams.get("owner") ?? "";
572
+
517
573
  try {
518
574
  const db = (this.store as any).db;
519
575
  const total = db.prepare("SELECT COUNT(*) as count FROM chunks").get() as any;
@@ -529,9 +585,12 @@ export class ViewerServer {
529
585
  }
530
586
  let embCount = 0;
531
587
  try { embCount = (db.prepare("SELECT COUNT(*) as count FROM embeddings").get() as any).count; } catch { /* table may not exist */ }
532
- const sessionList = db.prepare(
533
- "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",
534
- ).all() as any[];
588
+ const sessionQuery = ownerFilter
589
+ ? "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"
590
+ : "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";
591
+ const sessionList = (ownerFilter
592
+ ? db.prepare(sessionQuery).all(ownerFilter)
593
+ : db.prepare(sessionQuery).all()) as any[];
535
594
 
536
595
  let skillCount = 0;
537
596
  try { skillCount = (db.prepare("SELECT COUNT(*) as count FROM skills").get() as any).count; } catch { /* table may not exist yet */ }
@@ -683,8 +742,11 @@ export class ViewerServer {
683
742
  const relatedTasks = this.store.getTasksBySkill(skillId);
684
743
  const files = fs.existsSync(skill.dirPath) ? this.walkDir(skill.dirPath, skill.dirPath) : [];
685
744
 
745
+ const db = (this.store as any).db;
746
+ const sharedSkill = db.prepare("SELECT visibility, group_id FROM hub_skills WHERE source_skill_id = ? ORDER BY updated_at DESC LIMIT 1").get(skillId) as { visibility: string | null; group_id: string | null } | undefined;
747
+
686
748
  this.jsonResponse(res, {
687
- skill,
749
+ skill: { ...skill, sharingVisibility: sharedSkill?.visibility ?? null, sharingGroupId: sharedSkill?.group_id ?? null },
688
750
  versions: versions.map(v => ({
689
751
  id: v.id,
690
752
  version: v.version,
@@ -795,7 +857,7 @@ export class ViewerServer {
795
857
  private handleSkillVisibility(req: http.IncomingMessage, res: http.ServerResponse, urlPath: string): void {
796
858
  const segments = urlPath.split("/");
797
859
  const skillId = segments[segments.length - 2];
798
- this.readBody(req, (body) => {
860
+ this.readBody(req, async (body) => {
799
861
  try {
800
862
  const parsed = JSON.parse(body);
801
863
  const visibility = parsed.visibility;
@@ -811,7 +873,46 @@ export class ViewerServer {
811
873
  return;
812
874
  }
813
875
  this.store.setSkillVisibility(skillId, visibility);
814
- this.jsonResponse(res, { ok: true, skillId, visibility });
876
+
877
+ let hubSynced = false;
878
+ const sharing = this.ctx?.config?.sharing;
879
+ if (sharing?.enabled && this.ctx) {
880
+ try {
881
+ const hubClient = await this.resolveHubClientAware();
882
+ if (visibility === "public") {
883
+ const bundle = buildSkillBundleForHub(this.store, skillId);
884
+ const response = await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/publish", {
885
+ method: "POST",
886
+ body: JSON.stringify({ visibility: "public", groupId: null, metadata: bundle.metadata, bundle: bundle.bundle }),
887
+ }) as any;
888
+ if (hubClient.userId) {
889
+ const existing = this.store.getHubSkillBySource(hubClient.userId, skillId);
890
+ this.store.upsertHubSkill({
891
+ id: response?.skillId ?? existing?.id ?? crypto.randomUUID(),
892
+ sourceSkillId: skillId, sourceUserId: hubClient.userId,
893
+ name: skill.name, description: skill.description, version: skill.version,
894
+ groupId: null, visibility: "public",
895
+ bundle: JSON.stringify(bundle.bundle), qualityScore: skill.qualityScore,
896
+ createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(),
897
+ });
898
+ }
899
+ hubSynced = true;
900
+ this.log.info(`Skill "${skill.name}" published to Hub`);
901
+ } else {
902
+ await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/unpublish", {
903
+ method: "POST",
904
+ body: JSON.stringify({ sourceSkillId: skillId }),
905
+ });
906
+ if (hubClient.userId) this.store.deleteHubSkillBySource(hubClient.userId, skillId);
907
+ hubSynced = true;
908
+ this.log.info(`Skill "${skill.name}" unpublished from Hub`);
909
+ }
910
+ } catch (hubErr) {
911
+ this.log.warn(`Hub sync failed for skill visibility change: ${hubErr}`);
912
+ }
913
+ }
914
+
915
+ this.jsonResponse(res, { ok: true, skillId, visibility, hubSynced });
815
916
  } catch (err) {
816
917
  const errMsg = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
817
918
  this.log.error(`handleSkillVisibility error: skillId=${skillId}, body=${body}, err=${errMsg}`);
@@ -994,6 +1095,789 @@ export class ViewerServer {
994
1095
  return path.join(home, ".openclaw", "openclaw.json");
995
1096
  }
996
1097
 
1098
+ private getPluginEntryConfig(raw: any): Record<string, unknown> {
1099
+ const entries = raw?.plugins?.entries ?? {};
1100
+ return entries["memos-local-openclaw-plugin"]?.config
1101
+ ?? entries["memos-lite-openclaw-plugin"]?.config
1102
+ ?? entries["memos-lite"]?.config
1103
+ ?? {};
1104
+ }
1105
+
1106
+ private getResolvedViewerConfig(raw?: any): MemosLocalConfig {
1107
+ const pluginCfg = this.getPluginEntryConfig(raw);
1108
+ const stateDir = this.ctx?.stateDir ?? this.getOpenClawHome();
1109
+ return resolveConfig(pluginCfg as Partial<MemosLocalConfig>, stateDir);
1110
+ }
1111
+
1112
+ private hasUsableEmbeddingProvider(cfg: MemosLocalConfig): boolean {
1113
+ const embedding = cfg.embedding;
1114
+ if (!embedding?.provider) return false;
1115
+ if (embedding.provider === "openclaw") {
1116
+ return !!(this.ctx?.openclawAPI) && embedding.capabilities?.hostEmbedding === true;
1117
+ }
1118
+ return true;
1119
+ }
1120
+
1121
+ private hasUsableSummarizerProvider(cfg: MemosLocalConfig): boolean {
1122
+ const summarizer = cfg.summarizer;
1123
+ if (!summarizer?.provider) return false;
1124
+ if (summarizer.provider === "openclaw") {
1125
+ return !!(this.ctx?.openclawAPI) && summarizer.capabilities?.hostCompletion === true;
1126
+ }
1127
+ return true;
1128
+ }
1129
+
1130
+ private async serveSharingStatus(res: http.ServerResponse): Promise<void> {
1131
+ const sharing = this.ctx?.config?.sharing;
1132
+ const persisted = this.store.getClientHubConnection();
1133
+ const resolvedHubUrl = sharing?.client?.hubAddress ? normalizeHubUrl(sharing.client.hubAddress) : persisted?.hubUrl ?? null;
1134
+ const hasClientConfig = Boolean(
1135
+ (sharing?.client?.hubAddress && sharing?.client?.userToken) ||
1136
+ (persisted?.hubUrl && persisted?.userToken),
1137
+ );
1138
+ const base = {
1139
+ enabled: Boolean(sharing?.enabled),
1140
+ role: sharing?.role ?? null,
1141
+ clientConfigured: hasClientConfig,
1142
+ hubUrl: resolvedHubUrl,
1143
+ connection: { connected: false, user: null as any, hubUrl: undefined as string | undefined, teamName: null as string | null, apiVersion: null as string | null },
1144
+ admin: { canManageUsers: false, rejectSupported: false },
1145
+ };
1146
+
1147
+ if (!this.ctx || !sharing?.enabled) {
1148
+ this.jsonResponse(res, base);
1149
+ return;
1150
+ }
1151
+
1152
+ // Hub 模式下,本机就是管理者,直接赋予 admin 权限
1153
+ if (sharing.role === "hub") {
1154
+ base.admin.canManageUsers = true;
1155
+ base.admin.rejectSupported = true;
1156
+ base.connection.connected = true;
1157
+ base.connection.hubUrl = resolvedHubUrl ?? undefined;
1158
+
1159
+ // 通过 hub API 获取 admin 用户的真实信息(含分组)
1160
+ let adminUser: any = { username: "hub-admin", role: "admin", groups: [] };
1161
+ try {
1162
+ const hub = this.resolveHubConnection();
1163
+ if (hub) {
1164
+ const me = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/me", { method: "GET" }) as any;
1165
+ if (me) {
1166
+ adminUser = {
1167
+ id: me.id,
1168
+ username: me.username ?? "hub-admin",
1169
+ role: me.role ?? "admin",
1170
+ groups: Array.isArray(me.groups) ? me.groups : [],
1171
+ };
1172
+ }
1173
+ }
1174
+ } catch { /* fallback to default */ }
1175
+ base.connection.user = adminUser;
1176
+
1177
+ // Fetch team info from own hub
1178
+ try {
1179
+ const selfUrl = resolvedHubUrl || `http://localhost:${sharing.hub?.port ?? 21816}`;
1180
+ const info = await fetch(`${selfUrl}/api/v1/hub/info`).then(r => r.ok ? r.json() : null).catch(() => null) as any;
1181
+ base.connection.teamName = info?.teamName ?? sharing.hub?.teamName ?? null;
1182
+ base.connection.apiVersion = info?.apiVersion ?? null;
1183
+ } catch { /* ignore */ }
1184
+ this.jsonResponse(res, base);
1185
+ return;
1186
+ }
1187
+
1188
+ if (!hasClientConfig) {
1189
+ this.jsonResponse(res, base);
1190
+ return;
1191
+ }
1192
+
1193
+ try {
1194
+ const status = await getHubStatus(this.store, this.ctx.config);
1195
+ const output = { ...base, connection: { ...base.connection, ...status } } as any;
1196
+ if (status.connected && status.hubUrl) {
1197
+ try {
1198
+ const info = await fetch(`${status.hubUrl}/api/v1/hub/info`).then((r) => (r.ok ? r.json() : null)).catch(() => null) as any;
1199
+ output.connection.teamName = info?.teamName ?? null;
1200
+ output.connection.apiVersion = info?.apiVersion ?? null;
1201
+ } catch {}
1202
+ }
1203
+ output.admin.canManageUsers = status.connected && status.user?.role === "admin";
1204
+ output.admin.rejectSupported = output.admin.canManageUsers;
1205
+ this.jsonResponse(res, output);
1206
+ } catch (err) {
1207
+ this.jsonResponse(res, { ...base, error: String(err) });
1208
+ }
1209
+ }
1210
+
1211
+ private async serveSharingPendingUsers(res: http.ServerResponse): Promise<void> {
1212
+ if (!this.ctx) return this.jsonResponse(res, { users: [], error: "sharing_unavailable" });
1213
+ try {
1214
+ const hub = this.resolveHubConnection();
1215
+ if (!hub) return this.jsonResponse(res, { users: [], error: "not_configured" });
1216
+ const data = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/pending-users", { method: "GET" }) as any;
1217
+ this.jsonResponse(res, { users: Array.isArray(data?.users) ? data.users : [] });
1218
+ } catch (err) {
1219
+ this.jsonResponse(res, { users: [], error: String(err) });
1220
+ }
1221
+ }
1222
+
1223
+ private handleSharingApproveUser(req: http.IncomingMessage, res: http.ServerResponse): void {
1224
+ this.readBody(req, async (body) => {
1225
+ if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
1226
+ try {
1227
+ const parsed = JSON.parse(body || "{}");
1228
+ const hub = this.resolveHubConnection();
1229
+ if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
1230
+ const result = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/approve-user", {
1231
+ method: "POST",
1232
+ body: JSON.stringify({ userId: parsed.userId, username: parsed.username }),
1233
+ });
1234
+ this.jsonResponse(res, { ok: true, result });
1235
+ } catch (err) {
1236
+ this.jsonResponse(res, { ok: false, error: String(err) });
1237
+ }
1238
+ });
1239
+ }
1240
+
1241
+ private handleSharingRejectUser(req: http.IncomingMessage, res: http.ServerResponse): void {
1242
+ this.readBody(req, async (body) => {
1243
+ if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
1244
+ try {
1245
+ const parsed = JSON.parse(body || "{}");
1246
+ const hub = this.resolveHubConnection();
1247
+ if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
1248
+ const result = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/reject-user", {
1249
+ method: "POST",
1250
+ body: JSON.stringify({ userId: parsed.userId }),
1251
+ });
1252
+ this.jsonResponse(res, { ok: true, result });
1253
+ } catch (err) {
1254
+ this.jsonResponse(res, { ok: false, error: String(err) });
1255
+ }
1256
+ });
1257
+ }
1258
+
1259
+ private async serveSharingMemoryList(res: http.ServerResponse, url: URL): Promise<void> {
1260
+ if (!this.ctx) return this.jsonResponse(res, { memories: [], error: "sharing_unavailable" });
1261
+ try {
1262
+ const limit = Number(url.searchParams.get("limit") || 40);
1263
+ const data = await hubListMemories(this.store, this.ctx, { limit });
1264
+ this.jsonResponse(res, { memories: Array.isArray(data?.memories) ? data.memories : [] });
1265
+ } catch (err) {
1266
+ this.jsonResponse(res, { memories: [], error: String(err) });
1267
+ }
1268
+ }
1269
+
1270
+ private async serveSharingTaskList(res: http.ServerResponse, url: URL): Promise<void> {
1271
+ if (!this.ctx) return this.jsonResponse(res, { tasks: [], error: "sharing_unavailable" });
1272
+ try {
1273
+ const limit = Number(url.searchParams.get("limit") || 40);
1274
+ const data = await hubListTasks(this.store, this.ctx, { limit });
1275
+ this.jsonResponse(res, { tasks: Array.isArray(data?.tasks) ? data.tasks : [] });
1276
+ } catch (err) {
1277
+ this.jsonResponse(res, { tasks: [], error: String(err) });
1278
+ }
1279
+ }
1280
+
1281
+ private async serveSharingSkillList(res: http.ServerResponse, url: URL): Promise<void> {
1282
+ if (!this.ctx) return this.jsonResponse(res, { skills: [], error: "sharing_unavailable" });
1283
+ try {
1284
+ const limit = Number(url.searchParams.get("limit") || 40);
1285
+ const data = await hubListSkills(this.store, this.ctx, { limit });
1286
+ this.jsonResponse(res, { skills: Array.isArray(data?.skills) ? data.skills : [] });
1287
+ } catch (err) {
1288
+ this.jsonResponse(res, { skills: [], error: String(err) });
1289
+ }
1290
+ }
1291
+
1292
+ private handleSharingMemorySearch(req: http.IncomingMessage, res: http.ServerResponse): void {
1293
+ this.readBody(req, async (body) => {
1294
+ if (!this.ctx) return this.jsonResponse(res, { local: { hits: [], meta: {} }, hub: { hits: [], meta: { totalCandidates: 0, searchedGroups: [], includedPublic: false } }, error: "sharing_unavailable" });
1295
+ const emptyHub = { hits: [], meta: { totalCandidates: 0, searchedGroups: [], includedPublic: false } };
1296
+ try {
1297
+ const parsed = JSON.parse(body || "{}");
1298
+ const query = String(parsed.query || "");
1299
+ const role = typeof parsed.role === "string" ? parsed.role : undefined;
1300
+ const maxResults = typeof parsed.maxResults === "number" ? parsed.maxResults : 10;
1301
+ const scope = parsed.scope === "group" || parsed.scope === "all" ? parsed.scope : "local";
1302
+ const local = this.searchLocalViewerMemories(query, { role, maxResults });
1303
+ if (scope === "local") {
1304
+ return this.jsonResponse(res, { local: { hits: local.hits, meta: local.meta }, hub: emptyHub });
1305
+ }
1306
+ try {
1307
+ const hub = await hubSearchMemories(this.store, this.ctx, { query, maxResults, scope });
1308
+ this.jsonResponse(res, { local: { hits: local.hits, meta: local.meta }, hub });
1309
+ } catch (err) {
1310
+ this.jsonResponse(res, { local: { hits: local.hits, meta: local.meta }, hub: emptyHub, error: String(err) });
1311
+ }
1312
+ } catch (err) {
1313
+ this.jsonResponse(res, { local: { hits: [], meta: {} }, hub: emptyHub, error: String(err) });
1314
+ }
1315
+ });
1316
+ }
1317
+
1318
+ private handleSharingMemoryDetail(req: http.IncomingMessage, res: http.ServerResponse): void {
1319
+ this.readBody(req, async (body) => {
1320
+ if (!this.ctx) return this.jsonResponse(res, { error: "sharing_unavailable" });
1321
+ try {
1322
+ const parsed = JSON.parse(body || "{}");
1323
+ const detail = await hubGetMemoryDetail(this.store, this.ctx, { remoteHitId: String(parsed.remoteHitId || "") });
1324
+ this.jsonResponse(res, detail);
1325
+ } catch (err) {
1326
+ this.jsonResponse(res, { error: String(err) });
1327
+ }
1328
+ });
1329
+ }
1330
+
1331
+ private async serveSharingSkillSearch(res: http.ServerResponse, url: URL): Promise<void> {
1332
+ if (!this.ctx) return this.jsonResponse(res, { local: { hits: [] }, hub: { hits: [] }, error: "sharing_unavailable" });
1333
+ try {
1334
+ const query = String(url.searchParams.get("query") || "");
1335
+ const scope = url.searchParams.get("scope") === "group" || url.searchParams.get("scope") === "all" ? url.searchParams.get("scope")! : "local";
1336
+ const recall = new RecallEngine(this.store, this.embedder, this.ctx);
1337
+ const localHits = await recall.searchSkills(query, "mix" as any, "agent:main");
1338
+ if (scope === "local") {
1339
+ return this.jsonResponse(res, { local: { hits: localHits }, hub: { hits: [] } });
1340
+ }
1341
+ try {
1342
+ const hub = await hubSearchSkills(this.store, this.ctx, { query, maxResults: Number(url.searchParams.get("maxResults") || 20) });
1343
+ this.jsonResponse(res, { local: { hits: localHits }, hub });
1344
+ } catch (err) {
1345
+ this.jsonResponse(res, { local: { hits: localHits }, hub: { hits: [] }, error: String(err) });
1346
+ }
1347
+ } catch (err) {
1348
+ this.jsonResponse(res, { local: { hits: [] }, hub: { hits: [] }, error: String(err) });
1349
+ }
1350
+ }
1351
+
1352
+ private searchLocalViewerMemories(query: string, options?: { role?: string; maxResults?: number }): { hits: any[]; meta: Record<string, unknown> } {
1353
+ const db = (this.store as any).db;
1354
+ const role = options?.role;
1355
+ const maxResults = options?.maxResults ?? 10;
1356
+ const params: any[] = [];
1357
+ let rows: any[] = [];
1358
+ try {
1359
+ let sql = "SELECT c.* FROM chunks_fts f JOIN chunks c ON f.rowid = c.rowid WHERE chunks_fts MATCH ?";
1360
+ params.push(query);
1361
+ if (role) {
1362
+ sql += " AND c.role = ?";
1363
+ params.push(role);
1364
+ }
1365
+ sql += " ORDER BY rank LIMIT ?";
1366
+ params.push(maxResults);
1367
+ rows = db.prepare(sql).all(...params);
1368
+ } catch {
1369
+ const likeParams: any[] = [`%${query}%`, `%${query}%`];
1370
+ let sql = "SELECT * FROM chunks WHERE (content LIKE ? OR summary LIKE ?)";
1371
+ if (role) {
1372
+ sql += " AND role = ?";
1373
+ likeParams.push(role);
1374
+ }
1375
+ sql += " ORDER BY created_at DESC LIMIT ?";
1376
+ likeParams.push(maxResults);
1377
+ rows = db.prepare(sql).all(...likeParams);
1378
+ }
1379
+ const hits = rows.map((row: any, idx: number) => ({
1380
+ id: row.id,
1381
+ summary: row.summary || row.content?.slice(0, 120) || "",
1382
+ excerpt: row.content || "",
1383
+ score: Math.max(0.3, 1 - idx * 0.1),
1384
+ role: row.role,
1385
+ ref: { sessionKey: row.session_key, chunkId: row.id, turnId: row.turn_id, seq: row.seq },
1386
+ taskId: row.task_id ?? null,
1387
+ skillId: row.skill_id ?? null,
1388
+ }));
1389
+ return { hits, meta: { total: hits.length, usedMaxResults: maxResults } };
1390
+ }
1391
+
1392
+ private handleSharingTaskShare(req: http.IncomingMessage, res: http.ServerResponse): void {
1393
+ this.readBody(req, async (body) => {
1394
+ if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
1395
+ try {
1396
+ const parsed = JSON.parse(body || "{}");
1397
+ const taskId = String(parsed.taskId || "");
1398
+ const visibility = parsed.visibility === "group" ? "group" : "public";
1399
+ const groupId = typeof parsed.groupId === "string" ? parsed.groupId : undefined;
1400
+ const task = this.store.getTask(taskId);
1401
+ if (!task) return this.jsonResponse(res, { ok: false, error: "task_not_found" });
1402
+ const chunks = this.store.getChunksByTask(taskId);
1403
+ if (chunks.length === 0) return this.jsonResponse(res, { ok: false, error: "no_chunks" });
1404
+ const hubClient = await this.resolveHubClientAware();
1405
+ const response = await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/share", {
1406
+ method: "POST",
1407
+ body: JSON.stringify({
1408
+ task: {
1409
+ id: task.id,
1410
+ sourceTaskId: task.id,
1411
+ title: task.title,
1412
+ summary: task.summary,
1413
+ groupId: visibility === "group" ? groupId ?? null : null,
1414
+ visibility,
1415
+ createdAt: task.startedAt ?? Date.now(),
1416
+ updatedAt: task.updatedAt ?? Date.now(),
1417
+ },
1418
+ chunks: chunks.map((chunk) => ({
1419
+ id: chunk.id,
1420
+ hubTaskId: task.id,
1421
+ sourceTaskId: task.id,
1422
+ sourceChunkId: chunk.id,
1423
+ role: chunk.role,
1424
+ content: chunk.content,
1425
+ summary: chunk.summary,
1426
+ kind: chunk.kind,
1427
+ createdAt: chunk.createdAt,
1428
+ })),
1429
+ }),
1430
+ });
1431
+ const hubUserId = hubClient.userId;
1432
+ if (hubUserId) {
1433
+ this.store.upsertHubTask({
1434
+ id: task.id,
1435
+ sourceTaskId: task.id,
1436
+ sourceUserId: hubUserId,
1437
+ title: task.title,
1438
+ summary: task.summary,
1439
+ groupId: visibility === "group" ? groupId ?? null : null,
1440
+ visibility,
1441
+ createdAt: task.startedAt ?? Date.now(),
1442
+ updatedAt: task.updatedAt ?? Date.now(),
1443
+ });
1444
+ }
1445
+ this.jsonResponse(res, { ok: true, taskId, visibility, response });
1446
+ } catch (err) {
1447
+ this.jsonResponse(res, { ok: false, error: String(err) });
1448
+ }
1449
+ });
1450
+ }
1451
+
1452
+ private handleSharingTaskUnshare(req: http.IncomingMessage, res: http.ServerResponse): void {
1453
+ this.readBody(req, async (body) => {
1454
+ if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
1455
+ try {
1456
+ const parsed = JSON.parse(body || "{}");
1457
+ const taskId = String(parsed.taskId || "");
1458
+ const task = this.store.getTask(taskId);
1459
+ if (!task) return this.jsonResponse(res, { ok: false, error: "task_not_found" });
1460
+ const hubClient = await this.resolveHubClientAware();
1461
+ await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/unshare", {
1462
+ method: "POST",
1463
+ body: JSON.stringify({ sourceTaskId: task.id }),
1464
+ });
1465
+ const hubUserId = hubClient.userId;
1466
+ if (hubUserId) this.store.deleteHubTaskBySource(hubUserId, task.id);
1467
+ this.jsonResponse(res, { ok: true, taskId });
1468
+ } catch (err) {
1469
+ this.jsonResponse(res, { ok: false, error: String(err) });
1470
+ }
1471
+ });
1472
+ }
1473
+
1474
+ private handleSharingMemoryShare(req: http.IncomingMessage, res: http.ServerResponse): void {
1475
+ this.readBody(req, async (body) => {
1476
+ if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
1477
+ try {
1478
+ const parsed = JSON.parse(body || "{}");
1479
+ const chunkId = String(parsed.chunkId || "");
1480
+ const visibility = parsed.visibility === "group" ? "group" : "public";
1481
+ const groupId = typeof parsed.groupId === "string" ? parsed.groupId : undefined;
1482
+ const db = (this.store as any).db;
1483
+ const chunk = db.prepare("SELECT * FROM chunks WHERE id = ?").get(chunkId) as any;
1484
+ if (!chunk) return this.jsonResponse(res, { ok: false, error: "memory_not_found" });
1485
+ const hubClient = await this.resolveHubClientAware();
1486
+ const response = await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/share", {
1487
+ method: "POST",
1488
+ body: JSON.stringify({
1489
+ memory: {
1490
+ sourceChunkId: chunk.id,
1491
+ role: chunk.role,
1492
+ content: chunk.content,
1493
+ summary: chunk.summary,
1494
+ kind: chunk.kind,
1495
+ groupId: visibility === "group" ? groupId ?? null : null,
1496
+ visibility,
1497
+ },
1498
+ }),
1499
+ });
1500
+ const hubUserId = hubClient.userId;
1501
+ if (hubUserId) {
1502
+ const now = Date.now();
1503
+ const existing = this.store.getHubMemoryBySource(hubUserId, chunk.id);
1504
+ this.store.upsertHubMemory({
1505
+ id: (response as any)?.memoryId ?? existing?.id ?? crypto.randomUUID(),
1506
+ sourceChunkId: chunk.id,
1507
+ sourceUserId: hubUserId,
1508
+ role: chunk.role,
1509
+ content: chunk.content,
1510
+ summary: chunk.summary ?? "",
1511
+ kind: chunk.kind,
1512
+ groupId: visibility === "group" ? groupId ?? null : null,
1513
+ visibility,
1514
+ createdAt: existing?.createdAt ?? now,
1515
+ updatedAt: now,
1516
+ });
1517
+ }
1518
+ this.jsonResponse(res, { ok: true, chunkId, visibility, response });
1519
+ } catch (err) {
1520
+ this.jsonResponse(res, { ok: false, error: String(err) });
1521
+ }
1522
+ });
1523
+ }
1524
+
1525
+ private handleSharingMemoryUnshare(req: http.IncomingMessage, res: http.ServerResponse): void {
1526
+ this.readBody(req, async (body) => {
1527
+ if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
1528
+ try {
1529
+ const parsed = JSON.parse(body || "{}");
1530
+ const chunkId = String(parsed.chunkId || "");
1531
+ const hubClient = await this.resolveHubClientAware();
1532
+ await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/unshare", {
1533
+ method: "POST",
1534
+ body: JSON.stringify({ sourceChunkId: chunkId }),
1535
+ });
1536
+ const hubUserId = hubClient.userId;
1537
+ if (hubUserId) this.store.deleteHubMemoryBySource(hubUserId, chunkId);
1538
+ this.jsonResponse(res, { ok: true, chunkId });
1539
+ } catch (err) {
1540
+ this.jsonResponse(res, { ok: false, error: String(err) });
1541
+ }
1542
+ });
1543
+ }
1544
+
1545
+ private handleSharingSkillPull(req: http.IncomingMessage, res: http.ServerResponse): void {
1546
+ this.readBody(req, async (body) => {
1547
+ if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
1548
+ try {
1549
+ const parsed = JSON.parse(body || "{}");
1550
+ const skillId = String(parsed.skillId || "");
1551
+ const payload = await fetchHubSkillBundle(this.store, this.ctx, { skillId });
1552
+ const restored = restoreSkillBundleFromHub(this.store, this.ctx, payload);
1553
+ this.jsonResponse(res, { ok: true, pulled: true, hubSkillId: skillId, ...restored });
1554
+ } catch (err) {
1555
+ this.jsonResponse(res, { ok: false, error: String(err) });
1556
+ }
1557
+ });
1558
+ }
1559
+
1560
+ private handleSharingSkillShare(req: http.IncomingMessage, res: http.ServerResponse): void {
1561
+ this.readBody(req, async (body) => {
1562
+ if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
1563
+ try {
1564
+ const parsed = JSON.parse(body || "{}");
1565
+ const skillId = String(parsed.skillId || "");
1566
+ const visibility = parsed.visibility === "group" ? "group" : "public";
1567
+ const groupId = parsed.groupId ? String(parsed.groupId) : null;
1568
+ const skill = this.store.getSkill(skillId);
1569
+ if (!skill) return this.jsonResponse(res, { ok: false, error: "skill_not_found" });
1570
+ const bundle = buildSkillBundleForHub(this.store, skillId);
1571
+ const hubClient = await this.resolveHubClientAware();
1572
+ const response = await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/publish", {
1573
+ method: "POST",
1574
+ body: JSON.stringify({
1575
+ visibility,
1576
+ groupId: visibility === "group" ? groupId : null,
1577
+ metadata: bundle.metadata,
1578
+ bundle: bundle.bundle,
1579
+ }),
1580
+ });
1581
+ const hubUserId = hubClient.userId;
1582
+ if (hubUserId) {
1583
+ const existing = this.store.getHubSkillBySource(hubUserId, skillId);
1584
+ this.store.upsertHubSkill({
1585
+ id: (response as any)?.skillId ?? existing?.id ?? crypto.randomUUID(),
1586
+ sourceSkillId: skillId,
1587
+ sourceUserId: hubUserId,
1588
+ name: skill.name,
1589
+ description: skill.description,
1590
+ version: skill.version,
1591
+ groupId: visibility === "group" ? groupId : null,
1592
+ visibility,
1593
+ bundle: JSON.stringify(bundle.bundle),
1594
+ qualityScore: skill.qualityScore,
1595
+ createdAt: existing?.createdAt ?? Date.now(),
1596
+ updatedAt: Date.now(),
1597
+ });
1598
+ }
1599
+ this.jsonResponse(res, { ok: true, skillId, visibility, response });
1600
+ } catch (err) {
1601
+ this.jsonResponse(res, { ok: false, error: String(err) });
1602
+ }
1603
+ });
1604
+ }
1605
+
1606
+ private handleSharingSkillUnshare(req: http.IncomingMessage, res: http.ServerResponse): void {
1607
+ this.readBody(req, async (body) => {
1608
+ if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
1609
+ try {
1610
+ const parsed = JSON.parse(body || "{}");
1611
+ const skillId = String(parsed.skillId || "");
1612
+ const skill = this.store.getSkill(skillId);
1613
+ if (!skill) return this.jsonResponse(res, { ok: false, error: "skill_not_found" });
1614
+ const hubClient = await this.resolveHubClientAware();
1615
+ await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/unpublish", {
1616
+ method: "POST",
1617
+ body: JSON.stringify({ sourceSkillId: skill.id }),
1618
+ });
1619
+ const hubUserId = hubClient.userId;
1620
+ if (hubUserId) this.store.deleteHubSkillBySource(hubUserId, skill.id);
1621
+ this.jsonResponse(res, { ok: true, skillId });
1622
+ } catch (err) {
1623
+ this.jsonResponse(res, { ok: false, error: String(err) });
1624
+ }
1625
+ });
1626
+ }
1627
+
1628
+ private resolveHubConnection(): { hubUrl: string; userToken: string } | null {
1629
+ if (!this.ctx) return null;
1630
+
1631
+ // Hub 模式:连接自己,用 bootstrap admin token
1632
+ const sharing = this.ctx.config.sharing;
1633
+ if (sharing?.role === "hub") {
1634
+ const hubPort = sharing.hub?.port ?? 18800;
1635
+ const hubUrl = `http://127.0.0.1:${hubPort}`;
1636
+ try {
1637
+ const authPath = path.join(this.dataDir, "hub-auth.json");
1638
+ const authData = JSON.parse(fs.readFileSync(authPath, "utf8"));
1639
+ const adminToken = authData?.bootstrapAdminToken;
1640
+ if (adminToken) return { hubUrl, userToken: adminToken };
1641
+ } catch {
1642
+ // hub-auth.json 不存在或读取失败,fall through
1643
+ }
1644
+ }
1645
+
1646
+ // Client 模式:用配置的 hubAddress + userToken
1647
+ const conn = this.store.getClientHubConnection();
1648
+ const hubUrl = conn?.hubUrl || this.ctx.config.sharing?.client?.hubAddress || "";
1649
+ const userToken = conn?.userToken || this.ctx.config.sharing?.client?.userToken || "";
1650
+ if (!hubUrl || !userToken) return null;
1651
+ return { hubUrl: normalizeHubUrl(hubUrl), userToken };
1652
+ }
1653
+
1654
+ /** resolveHubClient 的 viewer 版本:hub 模式下使用 bootstrap admin 身份 */
1655
+ private async resolveHubClientAware(): Promise<ResolvedHubClient> {
1656
+ if (!this.ctx) throw new Error("sharing_unavailable");
1657
+ const sharing = this.ctx.config.sharing;
1658
+ if (sharing?.role === "hub") {
1659
+ const hub = this.resolveHubConnection();
1660
+ if (hub) {
1661
+ const me = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/me", { method: "GET" }) as any;
1662
+ return {
1663
+ hubUrl: hub.hubUrl,
1664
+ userToken: hub.userToken,
1665
+ userId: String(me.id),
1666
+ username: String(me.username ?? "hub-admin"),
1667
+ role: String(me.role ?? "admin"),
1668
+ };
1669
+ }
1670
+ }
1671
+ return resolveHubClient(this.store, this.ctx);
1672
+ }
1673
+
1674
+ private extractGroupId(path: string): string {
1675
+ const m = path.match(/\/api\/sharing\/groups\/([^/]+)/);
1676
+ return m ? decodeURIComponent(m[1]) : "";
1677
+ }
1678
+
1679
+ private async serveSharingGroups(res: http.ServerResponse): Promise<void> {
1680
+ const hub = this.resolveHubConnection();
1681
+ if (!hub) return this.jsonResponse(res, { groups: [], error: "not_configured" });
1682
+ try {
1683
+ const data = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/groups", { method: "GET" }) as any;
1684
+ this.jsonResponse(res, { groups: Array.isArray(data?.groups) ? data.groups : [] });
1685
+ } catch (err) {
1686
+ this.jsonResponse(res, { groups: [], error: String(err) });
1687
+ }
1688
+ }
1689
+
1690
+ private handleSharingGroupCreate(req: http.IncomingMessage, res: http.ServerResponse): void {
1691
+ this.readBody(req, async (body) => {
1692
+ const hub = this.resolveHubConnection();
1693
+ if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
1694
+ try {
1695
+ const parsed = JSON.parse(body || "{}");
1696
+ const data = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/groups", {
1697
+ method: "POST",
1698
+ body: JSON.stringify({ name: parsed.name, description: parsed.description }),
1699
+ }) as any;
1700
+ this.jsonResponse(res, { ok: true, ...data });
1701
+ } catch (err) {
1702
+ this.jsonResponse(res, { ok: false, error: String(err) });
1703
+ }
1704
+ });
1705
+ }
1706
+
1707
+ private handleSharingGroupUpdate(req: http.IncomingMessage, res: http.ServerResponse, p: string): void {
1708
+ this.readBody(req, async (body) => {
1709
+ const hub = this.resolveHubConnection();
1710
+ if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
1711
+ const groupId = this.extractGroupId(p);
1712
+ try {
1713
+ const parsed = JSON.parse(body || "{}");
1714
+ await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}`, {
1715
+ method: "PUT",
1716
+ body: JSON.stringify({ name: parsed.name, description: parsed.description }),
1717
+ });
1718
+ this.jsonResponse(res, { ok: true });
1719
+ } catch (err) {
1720
+ this.jsonResponse(res, { ok: false, error: String(err) });
1721
+ }
1722
+ });
1723
+ }
1724
+
1725
+ private async handleSharingGroupDelete(res: http.ServerResponse, p: string): Promise<void> {
1726
+ const hub = this.resolveHubConnection();
1727
+ if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
1728
+ const groupId = this.extractGroupId(p);
1729
+ try {
1730
+ await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}`, { method: "DELETE" });
1731
+ this.jsonResponse(res, { ok: true });
1732
+ } catch (err) {
1733
+ this.jsonResponse(res, { ok: false, error: String(err) });
1734
+ }
1735
+ }
1736
+
1737
+ private async serveSharingGroupMembers(res: http.ServerResponse, p: string): Promise<void> {
1738
+ const hub = this.resolveHubConnection();
1739
+ if (!hub) return this.jsonResponse(res, { members: [], error: "not_configured" });
1740
+ const groupId = this.extractGroupId(p);
1741
+ try {
1742
+ const data = await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}`, { method: "GET" }) as any;
1743
+ this.jsonResponse(res, { members: Array.isArray(data?.members) ? data.members : [] });
1744
+ } catch (err) {
1745
+ this.jsonResponse(res, { members: [], error: String(err) });
1746
+ }
1747
+ }
1748
+
1749
+ private handleSharingGroupAddMember(req: http.IncomingMessage, res: http.ServerResponse, p: string): void {
1750
+ this.readBody(req, async (body) => {
1751
+ const hub = this.resolveHubConnection();
1752
+ if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
1753
+ const groupId = this.extractGroupId(p);
1754
+ try {
1755
+ const parsed = JSON.parse(body || "{}");
1756
+ await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}/members`, {
1757
+ method: "POST",
1758
+ body: JSON.stringify({ userId: parsed.userId }),
1759
+ });
1760
+ this.jsonResponse(res, { ok: true });
1761
+ } catch (err) {
1762
+ this.jsonResponse(res, { ok: false, error: String(err) });
1763
+ }
1764
+ });
1765
+ }
1766
+
1767
+ private handleSharingGroupRemoveMember(req: http.IncomingMessage, res: http.ServerResponse, p: string): void {
1768
+ this.readBody(req, async (body) => {
1769
+ const hub = this.resolveHubConnection();
1770
+ if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
1771
+ const groupId = this.extractGroupId(p);
1772
+ try {
1773
+ const parsed = JSON.parse(body || "{}");
1774
+ await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}/members`, {
1775
+ method: "DELETE",
1776
+ body: JSON.stringify({ userId: parsed.userId }),
1777
+ });
1778
+ this.jsonResponse(res, { ok: true });
1779
+ } catch (err) {
1780
+ this.jsonResponse(res, { ok: false, error: String(err) });
1781
+ }
1782
+ });
1783
+ }
1784
+
1785
+ private async serveSharingUsers(res: http.ServerResponse): Promise<void> {
1786
+ const hub = this.resolveHubConnection();
1787
+ if (!hub) return this.jsonResponse(res, { users: [], error: "not_configured" });
1788
+ try {
1789
+ const data = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/users", { method: "GET" }) as any;
1790
+ this.jsonResponse(res, { users: Array.isArray(data?.users) ? data.users : [] });
1791
+ } catch (err) {
1792
+ this.jsonResponse(res, { users: [], error: String(err) });
1793
+ }
1794
+ }
1795
+
1796
+ // ─── Admin management endpoints (Hub-side data) ───
1797
+
1798
+ private async serveAdminSharedTasks(res: http.ServerResponse): Promise<void> {
1799
+ const hub = this.resolveHubConnection();
1800
+ if (!hub) return this.jsonResponse(res, { tasks: [], error: "not_configured" });
1801
+ try {
1802
+ const data = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/shared-tasks", { method: "GET" }) as any;
1803
+ this.jsonResponse(res, { tasks: Array.isArray(data?.tasks) ? data.tasks : [] });
1804
+ } catch (err) {
1805
+ this.jsonResponse(res, { tasks: [], error: String(err) });
1806
+ }
1807
+ }
1808
+
1809
+ private async handleAdminDeleteTask(res: http.ServerResponse, p: string): Promise<void> {
1810
+ const hub = this.resolveHubConnection();
1811
+ if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
1812
+ const taskId = decodeURIComponent(p.replace("/api/admin/shared-tasks/", ""));
1813
+ try {
1814
+ await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/admin/shared-tasks/${encodeURIComponent(taskId)}`, { method: "DELETE" });
1815
+ this.jsonResponse(res, { ok: true });
1816
+ } catch (err) {
1817
+ this.jsonResponse(res, { ok: false, error: String(err) });
1818
+ }
1819
+ }
1820
+
1821
+ private async serveAdminSharedSkills(res: http.ServerResponse): Promise<void> {
1822
+ const hub = this.resolveHubConnection();
1823
+ if (!hub) return this.jsonResponse(res, { skills: [], error: "not_configured" });
1824
+ try {
1825
+ const data = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/shared-skills", { method: "GET" }) as any;
1826
+ this.jsonResponse(res, { skills: Array.isArray(data?.skills) ? data.skills : [] });
1827
+ } catch (err) {
1828
+ this.jsonResponse(res, { skills: [], error: String(err) });
1829
+ }
1830
+ }
1831
+
1832
+ private async handleAdminDeleteSkill(res: http.ServerResponse, p: string): Promise<void> {
1833
+ const hub = this.resolveHubConnection();
1834
+ if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
1835
+ const skillId = decodeURIComponent(p.replace("/api/admin/shared-skills/", ""));
1836
+ try {
1837
+ await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/admin/shared-skills/${encodeURIComponent(skillId)}`, { method: "DELETE" });
1838
+ this.jsonResponse(res, { ok: true });
1839
+ } catch (err) {
1840
+ this.jsonResponse(res, { ok: false, error: String(err) });
1841
+ }
1842
+ }
1843
+
1844
+ private async serveAdminSharedMemories(res: http.ServerResponse): Promise<void> {
1845
+ const hub = this.resolveHubConnection();
1846
+ if (!hub) return this.jsonResponse(res, { memories: [], error: "not_configured" });
1847
+ try {
1848
+ const data = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/shared-memories", { method: "GET" }) as any;
1849
+ this.jsonResponse(res, { memories: Array.isArray(data?.memories) ? data.memories : [] });
1850
+ } catch (err) {
1851
+ this.jsonResponse(res, { memories: [], error: String(err) });
1852
+ }
1853
+ }
1854
+
1855
+ private async handleAdminDeleteMemory(res: http.ServerResponse, p: string): Promise<void> {
1856
+ const hub = this.resolveHubConnection();
1857
+ if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
1858
+ const memoryId = decodeURIComponent(p.replace("/api/admin/shared-memories/", ""));
1859
+ try {
1860
+ await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/admin/shared-memories/${encodeURIComponent(memoryId)}`, { method: "DELETE" });
1861
+ this.jsonResponse(res, { ok: true });
1862
+ } catch (err) {
1863
+ this.jsonResponse(res, { ok: false, error: String(err) });
1864
+ }
1865
+ }
1866
+
1867
+ private serveLocalIPs(res: http.ServerResponse): void {
1868
+ const nets = os.networkInterfaces();
1869
+ const ips: string[] = [];
1870
+ for (const name of Object.keys(nets)) {
1871
+ for (const net of nets[name] ?? []) {
1872
+ if (net.family === "IPv4" && !net.internal) {
1873
+ ips.push(net.address);
1874
+ }
1875
+ }
1876
+ }
1877
+ res.writeHead(200, { "Content-Type": "application/json" });
1878
+ res.end(JSON.stringify({ ips }));
1879
+ }
1880
+
997
1881
  private serveConfig(res: http.ServerResponse): void {
998
1882
  try {
999
1883
  const cfgPath = this.getOpenClawConfigPath();
@@ -1014,7 +1898,9 @@ export class ViewerServer {
1014
1898
  ?? entries["memos-lite-openclaw-plugin"]
1015
1899
  ?? entries["memos-lite"]
1016
1900
  ?? {};
1017
- if (pluginEntry.viewerPort == null && topEntry.viewerPort) {
1901
+ if ((pluginEntry as any).viewerPort != null) {
1902
+ result.viewerPort = (pluginEntry as any).viewerPort;
1903
+ } else if (topEntry.viewerPort) {
1018
1904
  result.viewerPort = topEntry.viewerPort;
1019
1905
  }
1020
1906
  this.jsonResponse(res, result);
@@ -1053,6 +1939,15 @@ export class ViewerServer {
1053
1939
  if (newCfg.skillEvolution) config.skillEvolution = newCfg.skillEvolution;
1054
1940
  if (newCfg.viewerPort) config.viewerPort = newCfg.viewerPort;
1055
1941
  if (newCfg.telemetry !== undefined) config.telemetry = newCfg.telemetry;
1942
+ if (newCfg.sharing !== undefined) {
1943
+ const existing = (config.sharing as Record<string, unknown>) || {};
1944
+ const merged = { ...existing, ...newCfg.sharing };
1945
+ // Deep-merge capabilities so new keys don't wipe existing ones
1946
+ if (newCfg.sharing.capabilities && existing.capabilities) {
1947
+ merged.capabilities = { ...(existing.capabilities as Record<string, unknown>), ...newCfg.sharing.capabilities };
1948
+ }
1949
+ config.sharing = merged;
1950
+ }
1056
1951
 
1057
1952
  fs.mkdirSync(path.dirname(cfgPath), { recursive: true });
1058
1953
  fs.writeFileSync(cfgPath, JSON.stringify(raw, null, 2), "utf-8");
@@ -1066,6 +1961,79 @@ export class ViewerServer {
1066
1961
  });
1067
1962
  }
1068
1963
 
1964
+ private handleUpdateUsername(req: http.IncomingMessage, res: http.ServerResponse): void {
1965
+ this.readBody(req, async (body) => {
1966
+ if (!this.ctx) return this.jsonResponse(res, { error: "sharing_unavailable" });
1967
+ try {
1968
+ const { username } = JSON.parse(body || "{}");
1969
+ if (!username || typeof username !== "string" || username.trim().length < 2 || username.trim().length > 32) {
1970
+ return this.jsonResponse(res, { error: "invalid_username" }, 400);
1971
+ }
1972
+ const trimmed = username.trim();
1973
+ const hubClient = await this.resolveHubClientAware();
1974
+ const result = await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/me/update-profile", {
1975
+ method: "POST",
1976
+ body: JSON.stringify({ username: trimmed }),
1977
+ }) as any;
1978
+ if (result.ok && result.userToken) {
1979
+ const sharing = this.ctx.config.sharing;
1980
+ if (sharing?.role === "hub") {
1981
+ try {
1982
+ const authPath = path.join(this.dataDir, "hub-auth.json");
1983
+ const authData = JSON.parse(fs.readFileSync(authPath, "utf8"));
1984
+ authData.bootstrapAdminToken = result.userToken;
1985
+ fs.writeFileSync(authPath, JSON.stringify(authData, null, 2), "utf-8");
1986
+ this.log.info("hub-auth.json updated with new admin token after username change");
1987
+ } catch (e) {
1988
+ this.log.warn(`Failed to update hub-auth.json: ${e}`);
1989
+ }
1990
+ } else {
1991
+ const persisted = this.store.getClientHubConnection();
1992
+ if (persisted) {
1993
+ this.store.setClientHubConnection({
1994
+ ...persisted,
1995
+ username: result.username,
1996
+ userToken: result.userToken,
1997
+ });
1998
+ }
1999
+ }
2000
+ }
2001
+ this.jsonResponse(res, result);
2002
+ } catch (err: any) {
2003
+ const msg = String(err?.message || err);
2004
+ if (msg.includes("409") || msg.includes("username_taken")) {
2005
+ return this.jsonResponse(res, { error: "username_taken" }, 409);
2006
+ }
2007
+ this.jsonResponse(res, { error: msg }, 500);
2008
+ }
2009
+ });
2010
+ }
2011
+
2012
+ private handleTestHubConnection(req: http.IncomingMessage, res: http.ServerResponse): void {
2013
+ this.readBody(req, async (body) => {
2014
+ try {
2015
+ const { hubUrl } = JSON.parse(body);
2016
+ if (!hubUrl) { this.jsonResponse(res, { ok: false, error: "hubUrl is required" }); return; }
2017
+ const url = hubUrl.replace(/\/+$/, "") + "/api/v1/hub/info";
2018
+ const ctrl = new AbortController();
2019
+ const timeout = setTimeout(() => ctrl.abort(), 8000);
2020
+ try {
2021
+ const r = await fetch(url, { signal: ctrl.signal });
2022
+ clearTimeout(timeout);
2023
+ if (!r.ok) { this.jsonResponse(res, { ok: false, error: `HTTP ${r.status}` }); return; }
2024
+ const info = await r.json() as Record<string, unknown>;
2025
+ this.jsonResponse(res, { ok: true, teamName: info.teamName || "", apiVersion: info.apiVersion || "" });
2026
+ } catch (e: unknown) {
2027
+ clearTimeout(timeout);
2028
+ const msg = e instanceof Error ? e.message : String(e);
2029
+ this.jsonResponse(res, { ok: false, error: msg.includes("abort") ? "Connection timeout (8s)" : msg });
2030
+ }
2031
+ } catch (e) {
2032
+ this.jsonResponse(res, { ok: false, error: String(e) });
2033
+ }
2034
+ });
2035
+ }
2036
+
1069
2037
  private handleTestModel(req: http.IncomingMessage, res: http.ServerResponse): void {
1070
2038
  this.readBody(req, async (body) => {
1071
2039
  try {
@@ -1177,34 +2145,119 @@ export class ViewerServer {
1177
2145
  req.on("data", (chunk: Buffer) => { body += chunk.toString(); });
1178
2146
  req.on("end", () => {
1179
2147
  try {
1180
- const { packageSpec } = JSON.parse(body);
1181
- if (!packageSpec || typeof packageSpec !== "string") {
2148
+ const { packageSpec: rawSpec } = JSON.parse(body);
2149
+ if (!rawSpec || typeof rawSpec !== "string") {
1182
2150
  res.writeHead(400, { "Content-Type": "application/json" });
1183
2151
  res.end(JSON.stringify({ ok: false, error: "Missing packageSpec" }));
1184
2152
  return;
1185
2153
  }
2154
+ const packageSpec = rawSpec.trim().replace(/^(?:npx\s+)?openclaw\s+plugins\s+install\s+/i, "");
1186
2155
  const allowed = /^@[\w-]+\/[\w.-]+(@[\w.-]+)?$/;
2156
+ this.log.info(`update-install: received packageSpec="${packageSpec}" (len=${packageSpec.length})`);
1187
2157
  if (!allowed.test(packageSpec)) {
2158
+ this.log.warn(`update-install: rejected packageSpec="${packageSpec}" — does not match ${allowed}`);
1188
2159
  res.writeHead(400, { "Content-Type": "application/json" });
1189
- res.end(JSON.stringify({ ok: false, error: "Invalid package spec" }));
2160
+ res.end(JSON.stringify({ ok: false, error: `Invalid package spec: "${packageSpec}"` }));
1190
2161
  return;
1191
2162
  }
1192
- this.log.info(`update-install: installing ${packageSpec}...`);
1193
- exec(`npx openclaw plugins install ${packageSpec}`, { timeout: 120_000 }, (err, stdout, stderr) => {
1194
- if (err) {
1195
- this.log.warn(`update-install failed: ${err.message}\n${stderr}`);
1196
- this.jsonResponse(res, { ok: false, error: stderr || err.message });
2163
+
2164
+ const pkgPath = this.findPluginPackageJson();
2165
+ const pluginName = pkgPath
2166
+ ? (() => { try { return JSON.parse(fs.readFileSync(pkgPath, "utf-8")).name; } catch { return null; } })()
2167
+ : null;
2168
+ const shortName = pluginName?.replace(/^@[\w-]+\//, "") ?? "memos-local-openclaw-plugin";
2169
+ const extDir = path.join(os.homedir(), ".openclaw", "extensions", shortName);
2170
+ const tmpDir = path.join(os.tmpdir(), `openclaw-update-${Date.now()}`);
2171
+
2172
+ // Download via npm pack, extract, and replace extension dir.
2173
+ // Does NOT touch openclaw.json → no config watcher SIGUSR1.
2174
+ this.log.info(`update-install: downloading ${packageSpec} via npm pack...`);
2175
+ fs.mkdirSync(tmpDir, { recursive: true });
2176
+ exec(`npm pack ${packageSpec} --pack-destination ${tmpDir}`, { timeout: 60_000 }, (packErr, packOut) => {
2177
+ if (packErr) {
2178
+ this.log.warn(`update-install: npm pack failed: ${packErr.message}`);
2179
+ this.jsonResponse(res, { ok: false, error: `Download failed: ${packErr.message}` });
2180
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
1197
2181
  return;
1198
2182
  }
1199
- this.log.info(`update-install success: ${stdout}`);
1200
- this.jsonResponse(res, { ok: true, output: stdout });
1201
- this.log.info(`update-install: restarting gateway...`);
1202
- setTimeout(() => {
1203
- exec("npx openclaw gateway restart", { timeout: 30_000 }, (restartErr) => {
1204
- if (restartErr) this.log.warn(`gateway restart failed: ${restartErr.message}`);
1205
- else this.log.info("gateway restart initiated");
2183
+ const tgzFile = packOut.trim().split("\n").pop()!;
2184
+ const tgzPath = path.join(tmpDir, tgzFile);
2185
+ this.log.info(`update-install: downloaded ${tgzFile}, extracting...`);
2186
+
2187
+ const extractDir = path.join(tmpDir, "extract");
2188
+ fs.mkdirSync(extractDir, { recursive: true });
2189
+ exec(`tar -xzf ${tgzPath} -C ${extractDir}`, { timeout: 30_000 }, (tarErr) => {
2190
+ if (tarErr) {
2191
+ this.log.warn(`update-install: tar extract failed: ${tarErr.message}`);
2192
+ this.jsonResponse(res, { ok: false, error: `Extract failed: ${tarErr.message}` });
2193
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
2194
+ return;
2195
+ }
2196
+
2197
+ // npm pack extracts to a "package" subdirectory
2198
+ const srcDir = path.join(extractDir, "package");
2199
+ if (!fs.existsSync(srcDir)) {
2200
+ this.jsonResponse(res, { ok: false, error: "Extracted package has no 'package' dir" });
2201
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
2202
+ return;
2203
+ }
2204
+
2205
+ // Replace extension directory
2206
+ this.log.info(`update-install: replacing ${extDir}...`);
2207
+ try { fs.rmSync(extDir, { recursive: true, force: true }); } catch {}
2208
+ fs.mkdirSync(path.dirname(extDir), { recursive: true });
2209
+ fs.renameSync(srcDir, extDir);
2210
+
2211
+ // Install dependencies
2212
+ this.log.info(`update-install: installing dependencies...`);
2213
+ exec(`cd ${extDir} && npm install --omit=dev --ignore-scripts`, { timeout: 120_000 }, (npmErr, npmOut, npmStderr) => {
2214
+ if (npmErr) {
2215
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
2216
+ this.log.warn(`update-install: npm install failed: ${npmErr.message}`);
2217
+ this.jsonResponse(res, { ok: false, error: `Dependency install failed: ${npmStderr || npmErr.message}` });
2218
+ return;
2219
+ }
2220
+
2221
+ // Rebuild native modules (do not swallow errors)
2222
+ exec(`cd ${extDir} && npm rebuild better-sqlite3`, { timeout: 60_000 }, (rebuildErr, rebuildOut, rebuildStderr) => {
2223
+ if (rebuildErr) {
2224
+ this.log.warn(`update-install: better-sqlite3 rebuild failed: ${rebuildErr.message}`);
2225
+ const stderr = String(rebuildStderr || "").trim();
2226
+ if (stderr) this.log.warn(`update-install: rebuild stderr: ${stderr.slice(0, 500)}`);
2227
+ // Continue so postinstall.cjs can run (it will try rebuild again and show user guidance)
2228
+ }
2229
+
2230
+ // Run postinstall.cjs: legacy cleanup, skill install, version marker, and optional sqlite re-check
2231
+ this.log.info(`update-install: running postinstall...`);
2232
+ exec(`cd ${extDir} && node scripts/postinstall.cjs`, { timeout: 180_000 }, (postErr, postOut, postStderr) => {
2233
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
2234
+
2235
+ if (postErr) {
2236
+ this.log.warn(`update-install: postinstall failed: ${postErr.message}`);
2237
+ const postStderrStr = String(postStderr || "").trim();
2238
+ if (postStderrStr) this.log.warn(`update-install: postinstall stderr: ${postStderrStr.slice(0, 500)}`);
2239
+ // Still report success; plugin is updated, user can run postinstall manually if needed
2240
+ }
2241
+
2242
+ // Read new version
2243
+ let newVersion = "unknown";
2244
+ try {
2245
+ const newPkg = JSON.parse(fs.readFileSync(path.join(extDir, "package.json"), "utf-8"));
2246
+ newVersion = newPkg.version ?? newVersion;
2247
+ } catch {}
2248
+
2249
+ this.log.info(`update-install: success! Updated to ${newVersion}`);
2250
+ this.jsonResponse(res, { ok: true, version: newVersion });
2251
+
2252
+ // Trigger Gateway restart after response is sent
2253
+ setTimeout(() => {
2254
+ this.log.info(`update-install: triggering gateway restart...`);
2255
+ process.kill(process.pid, "SIGUSR1");
2256
+ }, 500);
2257
+ });
2258
+ });
1206
2259
  });
1207
- }, 1000);
2260
+ });
1208
2261
  });
1209
2262
  } catch (e) {
1210
2263
  res.writeHead(400, { "Content-Type": "application/json" });
@@ -2279,8 +3332,8 @@ export class ViewerServer {
2279
3332
  req.on("end", () => cb(body));
2280
3333
  }
2281
3334
 
2282
- private jsonResponse(res: http.ServerResponse, data: unknown): void {
2283
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
3335
+ private jsonResponse(res: http.ServerResponse, data: unknown, statusCode = 200): void {
3336
+ res.writeHead(statusCode, { "Content-Type": "application/json; charset=utf-8" });
2284
3337
  res.end(JSON.stringify(data));
2285
3338
  }
2286
3339
  }