@memtensor/memos-local-openclaw-plugin 1.0.4-beta.4 → 1.0.4-beta.6

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 (152) hide show
  1. package/README.md +22 -39
  2. package/dist/capture/index.d.ts.map +1 -1
  3. package/dist/capture/index.js +6 -0
  4. package/dist/capture/index.js.map +1 -1
  5. package/dist/config.d.ts +1 -2
  6. package/dist/config.d.ts.map +1 -1
  7. package/dist/config.js +3 -72
  8. package/dist/config.js.map +1 -1
  9. package/dist/embedding/index.d.ts +2 -4
  10. package/dist/embedding/index.d.ts.map +1 -1
  11. package/dist/embedding/index.js +1 -17
  12. package/dist/embedding/index.js.map +1 -1
  13. package/dist/index.d.ts +0 -2
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +3 -4
  16. package/dist/index.js.map +1 -1
  17. package/dist/ingest/providers/index.d.ts +2 -10
  18. package/dist/ingest/providers/index.d.ts.map +1 -1
  19. package/dist/ingest/providers/index.js +43 -209
  20. package/dist/ingest/providers/index.js.map +1 -1
  21. package/dist/ingest/providers/openai.d.ts +0 -1
  22. package/dist/ingest/providers/openai.d.ts.map +1 -1
  23. package/dist/ingest/providers/openai.js +0 -1
  24. package/dist/ingest/providers/openai.js.map +1 -1
  25. package/dist/ingest/task-processor.js +1 -1
  26. package/dist/ingest/task-processor.js.map +1 -1
  27. package/dist/recall/engine.js +1 -1
  28. package/dist/recall/engine.js.map +1 -1
  29. package/dist/shared/llm-call.d.ts +2 -4
  30. package/dist/shared/llm-call.d.ts.map +1 -1
  31. package/dist/shared/llm-call.js +81 -20
  32. package/dist/shared/llm-call.js.map +1 -1
  33. package/dist/skill/evaluator.d.ts.map +1 -1
  34. package/dist/skill/evaluator.js +2 -2
  35. package/dist/skill/evaluator.js.map +1 -1
  36. package/dist/skill/evolver.d.ts +2 -0
  37. package/dist/skill/evolver.d.ts.map +1 -1
  38. package/dist/skill/evolver.js +3 -0
  39. package/dist/skill/evolver.js.map +1 -1
  40. package/dist/skill/generator.d.ts.map +1 -1
  41. package/dist/skill/generator.js +4 -4
  42. package/dist/skill/generator.js.map +1 -1
  43. package/dist/skill/upgrader.js +1 -1
  44. package/dist/skill/upgrader.js.map +1 -1
  45. package/dist/skill/validator.js +1 -1
  46. package/dist/skill/validator.js.map +1 -1
  47. package/dist/storage/ensure-binding.d.ts.map +1 -1
  48. package/dist/storage/ensure-binding.js +1 -3
  49. package/dist/storage/ensure-binding.js.map +1 -1
  50. package/dist/storage/sqlite.d.ts +0 -294
  51. package/dist/storage/sqlite.d.ts.map +1 -1
  52. package/dist/storage/sqlite.js +0 -821
  53. package/dist/storage/sqlite.js.map +1 -1
  54. package/dist/telemetry.d.ts +12 -5
  55. package/dist/telemetry.d.ts.map +1 -1
  56. package/dist/telemetry.js +135 -38
  57. package/dist/telemetry.js.map +1 -1
  58. package/dist/tools/index.d.ts +0 -1
  59. package/dist/tools/index.d.ts.map +1 -1
  60. package/dist/tools/index.js +1 -3
  61. package/dist/tools/index.js.map +1 -1
  62. package/dist/tools/memory-search.d.ts +2 -3
  63. package/dist/tools/memory-search.d.ts.map +1 -1
  64. package/dist/tools/memory-search.js +7 -48
  65. package/dist/tools/memory-search.js.map +1 -1
  66. package/dist/types.d.ts +2 -49
  67. package/dist/types.d.ts.map +1 -1
  68. package/dist/types.js.map +1 -1
  69. package/dist/viewer/html.d.ts.map +1 -1
  70. package/dist/viewer/html.js +471 -2974
  71. package/dist/viewer/html.js.map +1 -1
  72. package/dist/viewer/server.d.ts +0 -45
  73. package/dist/viewer/server.d.ts.map +1 -1
  74. package/dist/viewer/server.js +18 -1155
  75. package/dist/viewer/server.js.map +1 -1
  76. package/index.ts +42 -430
  77. package/openclaw.plugin.json +1 -2
  78. package/package.json +3 -4
  79. package/scripts/postinstall.cjs +46 -283
  80. package/skill/memos-memory-guide/SKILL.md +2 -26
  81. package/src/capture/index.ts +8 -0
  82. package/src/config.ts +3 -94
  83. package/src/embedding/index.ts +1 -21
  84. package/src/index.ts +4 -7
  85. package/src/ingest/providers/index.ts +46 -246
  86. package/src/ingest/providers/openai.ts +1 -1
  87. package/src/ingest/task-processor.ts +1 -1
  88. package/src/recall/engine.ts +1 -1
  89. package/src/shared/llm-call.ts +95 -23
  90. package/src/skill/evaluator.ts +2 -3
  91. package/src/skill/evolver.ts +5 -0
  92. package/src/skill/generator.ts +4 -6
  93. package/src/skill/upgrader.ts +1 -1
  94. package/src/skill/validator.ts +1 -1
  95. package/src/storage/ensure-binding.ts +1 -3
  96. package/src/storage/sqlite.ts +0 -1085
  97. package/src/telemetry.ts +152 -39
  98. package/src/tools/index.ts +0 -1
  99. package/src/tools/memory-search.ts +8 -57
  100. package/src/types.ts +2 -44
  101. package/src/viewer/html.ts +471 -2974
  102. package/src/viewer/server.ts +21 -1070
  103. package/dist/client/connector.d.ts +0 -30
  104. package/dist/client/connector.d.ts.map +0 -1
  105. package/dist/client/connector.js +0 -219
  106. package/dist/client/connector.js.map +0 -1
  107. package/dist/client/hub.d.ts +0 -61
  108. package/dist/client/hub.d.ts.map +0 -1
  109. package/dist/client/hub.js +0 -148
  110. package/dist/client/hub.js.map +0 -1
  111. package/dist/client/skill-sync.d.ts +0 -29
  112. package/dist/client/skill-sync.d.ts.map +0 -1
  113. package/dist/client/skill-sync.js +0 -216
  114. package/dist/client/skill-sync.js.map +0 -1
  115. package/dist/hub/auth.d.ts +0 -19
  116. package/dist/hub/auth.d.ts.map +0 -1
  117. package/dist/hub/auth.js +0 -70
  118. package/dist/hub/auth.js.map +0 -1
  119. package/dist/hub/server.d.ts +0 -41
  120. package/dist/hub/server.d.ts.map +0 -1
  121. package/dist/hub/server.js +0 -747
  122. package/dist/hub/server.js.map +0 -1
  123. package/dist/hub/user-manager.d.ts +0 -29
  124. package/dist/hub/user-manager.d.ts.map +0 -1
  125. package/dist/hub/user-manager.js +0 -125
  126. package/dist/hub/user-manager.js.map +0 -1
  127. package/dist/openclaw-api.d.ts +0 -53
  128. package/dist/openclaw-api.d.ts.map +0 -1
  129. package/dist/openclaw-api.js +0 -189
  130. package/dist/openclaw-api.js.map +0 -1
  131. package/dist/sharing/types.contract.d.ts +0 -2
  132. package/dist/sharing/types.contract.d.ts.map +0 -1
  133. package/dist/sharing/types.contract.js +0 -3
  134. package/dist/sharing/types.contract.js.map +0 -1
  135. package/dist/sharing/types.d.ts +0 -80
  136. package/dist/sharing/types.d.ts.map +0 -1
  137. package/dist/sharing/types.js +0 -3
  138. package/dist/sharing/types.js.map +0 -1
  139. package/dist/tools/network-memory-detail.d.ts +0 -4
  140. package/dist/tools/network-memory-detail.d.ts.map +0 -1
  141. package/dist/tools/network-memory-detail.js +0 -34
  142. package/dist/tools/network-memory-detail.js.map +0 -1
  143. package/src/client/connector.ts +0 -218
  144. package/src/client/hub.ts +0 -189
  145. package/src/client/skill-sync.ts +0 -202
  146. package/src/hub/auth.ts +0 -78
  147. package/src/hub/server.ts +0 -740
  148. package/src/hub/user-manager.ts +0 -139
  149. package/src/openclaw-api.ts +0 -287
  150. package/src/sharing/types.contract.ts +0 -40
  151. package/src/sharing/types.ts +0 -102
  152. package/src/tools/network-memory-detail.ts +0 -34
@@ -14,11 +14,7 @@ import { vectorSearch } from "../storage/vector";
14
14
  import { TaskProcessor } from "../ingest/task-processor";
15
15
  import { RecallEngine } from "../recall/engine";
16
16
  import { SkillEvolver } from "../skill/evolver";
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
+ import type { Logger, Chunk, PluginContext } from "../types";
22
18
  import { viewerHTML } from "./html";
23
19
  import { v4 as uuid } from "uuid";
24
20
 
@@ -224,7 +220,7 @@ export class ViewerServer {
224
220
  }
225
221
 
226
222
  if (p === "/api/memories" && req.method === "GET") this.serveMemories(res, url);
227
- else if (p === "/api/stats") this.serveStats(res, url);
223
+ else if (p === "/api/stats") this.serveStats(res);
228
224
  else if (p === "/api/metrics") this.serveMetrics(res, url);
229
225
  else if (p === "/api/tool-metrics") this.serveToolMetrics(res, url);
230
226
  else if (p === "/api/search") this.serveSearch(req, res, url);
@@ -247,41 +243,6 @@ export class ViewerServer {
247
243
  else if (p === "/api/memories" && req.method === "DELETE") this.handleDeleteAll(res);
248
244
  else if (p === "/api/logs" && req.method === "GET") this.serveLogs(res, url);
249
245
  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/retry-join" && req.method === "POST") this.handleRetryJoin(req, res);
255
- else if (p === "/api/sharing/search/memories" && req.method === "POST") this.handleSharingMemorySearch(req, res);
256
- else if (p === "/api/sharing/memories/list" && req.method === "GET") this.serveSharingMemoryList(res, url);
257
- else if (p === "/api/sharing/tasks/list" && req.method === "GET") this.serveSharingTaskList(res, url);
258
- else if (p === "/api/sharing/skills/list" && req.method === "GET") this.serveSharingSkillList(res, url);
259
- else if (p === "/api/sharing/memory-detail" && req.method === "POST") this.handleSharingMemoryDetail(req, res);
260
- else if (p === "/api/sharing/search/skills" && req.method === "GET") this.serveSharingSkillSearch(res, url);
261
- else if (p === "/api/sharing/tasks/share" && req.method === "POST") this.handleSharingTaskShare(req, res);
262
- else if (p === "/api/sharing/tasks/unshare" && req.method === "POST") this.handleSharingTaskUnshare(req, res);
263
- else if (p === "/api/sharing/update-username" && req.method === "POST") this.handleUpdateUsername(req, res);
264
- else if (p === "/api/sharing/test-hub" && req.method === "POST") this.handleTestHubConnection(req, res);
265
- else if (p === "/api/sharing/memories/share" && req.method === "POST") this.handleSharingMemoryShare(req, res);
266
- else if (p === "/api/sharing/memories/unshare" && req.method === "POST") this.handleSharingMemoryUnshare(req, res);
267
- else if (p === "/api/sharing/skills/pull" && req.method === "POST") this.handleSharingSkillPull(req, res);
268
- else if (p === "/api/sharing/skills/share" && req.method === "POST") this.handleSharingSkillShare(req, res);
269
- else if (p === "/api/sharing/skills/unshare" && req.method === "POST") this.handleSharingSkillUnshare(req, res);
270
- else if (p === "/api/sharing/groups" && req.method === "GET") this.serveSharingGroups(res);
271
- else if (p === "/api/sharing/groups" && req.method === "POST") this.handleSharingGroupCreate(req, res);
272
- else if (p.match(/^\/api\/sharing\/groups\/[^/]+$/) && req.method === "PUT") this.handleSharingGroupUpdate(req, res, p);
273
- else if (p.match(/^\/api\/sharing\/groups\/[^/]+$/) && req.method === "DELETE") this.handleSharingGroupDelete(res, p);
274
- else if (p.match(/^\/api\/sharing\/groups\/[^/]+\/members$/) && req.method === "GET") this.serveSharingGroupMembers(res, p);
275
- else if (p.match(/^\/api\/sharing\/groups\/[^/]+\/members$/) && req.method === "POST") this.handleSharingGroupAddMember(req, res, p);
276
- else if (p.match(/^\/api\/sharing\/groups\/[^/]+\/members$/) && req.method === "DELETE") this.handleSharingGroupRemoveMember(req, res, p);
277
- else if (p === "/api/sharing/users" && req.method === "GET") this.serveSharingUsers(res);
278
- else if (p === "/api/admin/shared-tasks" && req.method === "GET") this.serveAdminSharedTasks(res);
279
- else if (p.match(/^\/api\/admin\/shared-tasks\/[^/]+$/) && req.method === "DELETE") this.handleAdminDeleteTask(res, p);
280
- else if (p === "/api/admin/shared-skills" && req.method === "GET") this.serveAdminSharedSkills(res);
281
- else if (p.match(/^\/api\/admin\/shared-skills\/[^/]+$/) && req.method === "DELETE") this.handleAdminDeleteSkill(res, p);
282
- else if (p === "/api/admin/shared-memories" && req.method === "GET") this.serveAdminSharedMemories(res);
283
- else if (p.match(/^\/api\/admin\/shared-memories\/[^/]+$/) && req.method === "DELETE") this.handleAdminDeleteMemory(res, p);
284
- else if (p === "/api/local-ips" && req.method === "GET") this.serveLocalIPs(res);
285
246
  else if (p === "/api/config" && req.method === "GET") this.serveConfig(res);
286
247
  else if (p === "/api/config" && req.method === "PUT") this.handleSaveConfig(req, res);
287
248
  else if (p === "/api/test-model" && req.method === "POST") this.handleTestModel(req, res);
@@ -440,27 +401,15 @@ export class ViewerServer {
440
401
  const totalRow = db.prepare("SELECT COUNT(*) as count FROM chunks" + where).get(...params) as any;
441
402
  const rawMemories = db.prepare("SELECT * FROM chunks" + where + ` ORDER BY created_at ${sortBy} LIMIT ? OFFSET ?`).all(...params, limit, offset);
442
403
  const findMergeSources = db.prepare("SELECT id, summary, role FROM chunks WHERE dedup_target = ? AND (dedup_status = 'merged' OR dedup_status = 'duplicate')");
443
-
444
- const chunkIds = rawMemories.map((m: any) => m.id);
445
- const sharingMap = new Map<string, { visibility: string; group_id: string | null }>();
446
- if (chunkIds.length > 0) {
447
- try {
448
- const placeholders = chunkIds.map(() => "?").join(",");
449
- 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 }>;
450
- for (const r of sharedRows) sharingMap.set(r.source_chunk_id, r);
451
- } catch {
452
- }
453
- }
454
404
  const memories = rawMemories.map((m: any) => {
455
- const out: any = m.role === "user" && m.content ? { ...m, content: stripInboundMetadata(m.content) } : { ...m };
456
- if (out.merge_count > 0) {
405
+ if (m.role === "user" && m.content) {
406
+ m = { ...m, content: stripInboundMetadata(m.content) };
407
+ }
408
+ if (m.merge_count > 0) {
457
409
  const sources = findMergeSources.all(m.id) as Array<{ id: string; summary: string; role: string }>;
458
- out.merge_sources = sources;
410
+ m.merge_sources = sources;
459
411
  }
460
- const shared = sharingMap.get(m.id);
461
- out.sharingVisibility = shared?.visibility ?? null;
462
- out.sharingGroupId = shared?.group_id ?? null;
463
- return out;
412
+ return m;
464
413
  });
465
414
 
466
415
  this.store.recordViewerEvent("list");
@@ -536,7 +485,6 @@ export class ViewerServer {
536
485
  const db = (this.store as any).db;
537
486
  const meta = db.prepare("SELECT skill_status, skill_reason FROM tasks WHERE id = ?").get(taskId) as
538
487
  { skill_status: string | null; skill_reason: string | null } | undefined;
539
- 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;
540
488
 
541
489
  this.jsonResponse(res, {
542
490
  id: task.id,
@@ -550,12 +498,10 @@ export class ViewerServer {
550
498
  skillStatus: meta?.skill_status ?? null,
551
499
  skillReason: meta?.skill_reason ?? null,
552
500
  skillLinks,
553
- sharingVisibility: sharedTask?.visibility ?? null,
554
- sharingGroupId: sharedTask?.group_id ?? null,
555
501
  });
556
502
  }
557
503
 
558
- private serveStats(res: http.ServerResponse, url?: URL): void {
504
+ private serveStats(res: http.ServerResponse): void {
559
505
  const emptyStats = {
560
506
  totalMemories: 0, totalSessions: 0, totalEmbeddings: 0, totalSkills: 0,
561
507
  embeddingProvider: this.embedder?.provider ?? "none",
@@ -569,8 +515,6 @@ export class ViewerServer {
569
515
  return;
570
516
  }
571
517
 
572
- const ownerFilter = url?.searchParams.get("owner") ?? "";
573
-
574
518
  try {
575
519
  const db = (this.store as any).db;
576
520
  const total = db.prepare("SELECT COUNT(*) as count FROM chunks").get() as any;
@@ -586,12 +530,9 @@ export class ViewerServer {
586
530
  }
587
531
  let embCount = 0;
588
532
  try { embCount = (db.prepare("SELECT COUNT(*) as count FROM embeddings").get() as any).count; } catch { /* table may not exist */ }
589
- const sessionQuery = ownerFilter
590
- ? "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"
591
- : "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";
592
- const sessionList = (ownerFilter
593
- ? db.prepare(sessionQuery).all(ownerFilter)
594
- : db.prepare(sessionQuery).all()) as any[];
533
+ const sessionList = db.prepare(
534
+ "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",
535
+ ).all() as any[];
595
536
 
596
537
  let skillCount = 0;
597
538
  try { skillCount = (db.prepare("SELECT COUNT(*) as count FROM skills").get() as any).count; } catch { /* table may not exist yet */ }
@@ -743,11 +684,8 @@ export class ViewerServer {
743
684
  const relatedTasks = this.store.getTasksBySkill(skillId);
744
685
  const files = fs.existsSync(skill.dirPath) ? this.walkDir(skill.dirPath, skill.dirPath) : [];
745
686
 
746
- const db = (this.store as any).db;
747
- 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;
748
-
749
687
  this.jsonResponse(res, {
750
- skill: { ...skill, sharingVisibility: sharedSkill?.visibility ?? null, sharingGroupId: sharedSkill?.group_id ?? null },
688
+ skill,
751
689
  versions: versions.map(v => ({
752
690
  id: v.id,
753
691
  version: v.version,
@@ -858,7 +796,7 @@ export class ViewerServer {
858
796
  private handleSkillVisibility(req: http.IncomingMessage, res: http.ServerResponse, urlPath: string): void {
859
797
  const segments = urlPath.split("/");
860
798
  const skillId = segments[segments.length - 2];
861
- this.readBody(req, async (body) => {
799
+ this.readBody(req, (body) => {
862
800
  try {
863
801
  const parsed = JSON.parse(body);
864
802
  const visibility = parsed.visibility;
@@ -874,46 +812,7 @@ export class ViewerServer {
874
812
  return;
875
813
  }
876
814
  this.store.setSkillVisibility(skillId, visibility);
877
-
878
- let hubSynced = false;
879
- const sharing = this.ctx?.config?.sharing;
880
- if (sharing?.enabled && this.ctx) {
881
- try {
882
- const hubClient = await this.resolveHubClientAware();
883
- if (visibility === "public") {
884
- const bundle = buildSkillBundleForHub(this.store, skillId);
885
- const response = await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/publish", {
886
- method: "POST",
887
- body: JSON.stringify({ visibility: "public", groupId: null, metadata: bundle.metadata, bundle: bundle.bundle }),
888
- }) as any;
889
- if (hubClient.userId) {
890
- const existing = this.store.getHubSkillBySource(hubClient.userId, skillId);
891
- this.store.upsertHubSkill({
892
- id: response?.skillId ?? existing?.id ?? crypto.randomUUID(),
893
- sourceSkillId: skillId, sourceUserId: hubClient.userId,
894
- name: skill.name, description: skill.description, version: skill.version,
895
- groupId: null, visibility: "public",
896
- bundle: JSON.stringify(bundle.bundle), qualityScore: skill.qualityScore,
897
- createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(),
898
- });
899
- }
900
- hubSynced = true;
901
- this.log.info(`Skill "${skill.name}" published to Hub`);
902
- } else {
903
- await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/unpublish", {
904
- method: "POST",
905
- body: JSON.stringify({ sourceSkillId: skillId }),
906
- });
907
- if (hubClient.userId) this.store.deleteHubSkillBySource(hubClient.userId, skillId);
908
- hubSynced = true;
909
- this.log.info(`Skill "${skill.name}" unpublished from Hub`);
910
- }
911
- } catch (hubErr) {
912
- this.log.warn(`Hub sync failed for skill visibility change: ${hubErr}`);
913
- }
914
- }
915
-
916
- this.jsonResponse(res, { ok: true, skillId, visibility, hubSynced });
815
+ this.jsonResponse(res, { ok: true, skillId, visibility });
917
816
  } catch (err) {
918
817
  const errMsg = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
919
818
  this.log.error(`handleSkillVisibility error: skillId=${skillId}, body=${body}, err=${errMsg}`);
@@ -1093,843 +992,8 @@ export class ViewerServer {
1093
992
 
1094
993
  private getOpenClawConfigPath(): string {
1095
994
  const home = process.env.HOME || process.env.USERPROFILE || "";
1096
- return path.join(home, ".openclaw", "openclaw.json");
1097
- }
1098
-
1099
- private getPluginEntryConfig(raw: any): Record<string, unknown> {
1100
- const entries = raw?.plugins?.entries ?? {};
1101
- return entries["memos-local-openclaw-plugin"]?.config
1102
- ?? entries["memos-lite-openclaw-plugin"]?.config
1103
- ?? entries["memos-lite"]?.config
1104
- ?? {};
1105
- }
1106
-
1107
- private getResolvedViewerConfig(raw?: any): MemosLocalConfig {
1108
- const pluginCfg = this.getPluginEntryConfig(raw);
1109
- const stateDir = this.ctx?.stateDir ?? this.getOpenClawHome();
1110
- return resolveConfig(pluginCfg as Partial<MemosLocalConfig>, stateDir);
1111
- }
1112
-
1113
- private hasUsableEmbeddingProvider(cfg: MemosLocalConfig): boolean {
1114
- const embedding = cfg.embedding;
1115
- if (!embedding?.provider) return false;
1116
- if (embedding.provider === "openclaw") {
1117
- return !!(this.ctx?.openclawAPI) && embedding.capabilities?.hostEmbedding === true;
1118
- }
1119
- return true;
1120
- }
1121
-
1122
- private hasUsableSummarizerProvider(cfg: MemosLocalConfig): boolean {
1123
- const summarizer = cfg.summarizer;
1124
- if (!summarizer?.provider) return false;
1125
- if (summarizer.provider === "openclaw") {
1126
- return !!(this.ctx?.openclawAPI) && summarizer.capabilities?.hostCompletion === true;
1127
- }
1128
- return true;
1129
- }
1130
-
1131
- private async serveSharingStatus(res: http.ServerResponse): Promise<void> {
1132
- const sharing = this.ctx?.config?.sharing;
1133
- const persisted = this.store.getClientHubConnection();
1134
- const resolvedHubUrl = sharing?.client?.hubAddress ? normalizeHubUrl(sharing.client.hubAddress) : persisted?.hubUrl ?? null;
1135
- const hasClientConfig = Boolean(
1136
- (sharing?.client?.hubAddress && sharing?.client?.userToken) ||
1137
- (persisted?.hubUrl && persisted?.userToken),
1138
- );
1139
- const base = {
1140
- enabled: Boolean(sharing?.enabled),
1141
- role: sharing?.role ?? null,
1142
- clientConfigured: hasClientConfig,
1143
- hubUrl: resolvedHubUrl,
1144
- connection: { connected: false, user: null as any, hubUrl: undefined as string | undefined, teamName: null as string | null, apiVersion: null as string | null },
1145
- admin: { canManageUsers: false, rejectSupported: false },
1146
- };
1147
-
1148
- if (!this.ctx || !sharing?.enabled) {
1149
- this.jsonResponse(res, base);
1150
- return;
1151
- }
1152
-
1153
- // Hub 模式下,本机就是管理者,直接赋予 admin 权限
1154
- if (sharing.role === "hub") {
1155
- base.admin.canManageUsers = true;
1156
- base.admin.rejectSupported = true;
1157
- base.connection.connected = true;
1158
- base.connection.hubUrl = resolvedHubUrl ?? undefined;
1159
-
1160
- // 通过 hub API 获取 admin 用户的真实信息(含分组)
1161
- let adminUser: any = { username: "hub-admin", role: "admin", groups: [] };
1162
- try {
1163
- const hub = this.resolveHubConnection();
1164
- if (hub) {
1165
- const me = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/me", { method: "GET" }) as any;
1166
- if (me) {
1167
- adminUser = {
1168
- id: me.id,
1169
- username: me.username ?? "hub-admin",
1170
- role: me.role ?? "admin",
1171
- groups: Array.isArray(me.groups) ? me.groups : [],
1172
- };
1173
- }
1174
- }
1175
- } catch { /* fallback to default */ }
1176
- base.connection.user = adminUser;
1177
-
1178
- // Fetch team info from own hub
1179
- try {
1180
- const selfUrl = resolvedHubUrl || `http://localhost:${sharing.hub?.port ?? 21816}`;
1181
- const info = await fetch(`${selfUrl}/api/v1/hub/info`).then(r => r.ok ? r.json() : null).catch(() => null) as any;
1182
- base.connection.teamName = info?.teamName ?? sharing.hub?.teamName ?? null;
1183
- base.connection.apiVersion = info?.apiVersion ?? null;
1184
- } catch { /* ignore */ }
1185
- this.jsonResponse(res, base);
1186
- return;
1187
- }
1188
-
1189
- const hasPendingConnection = Boolean(persisted?.hubUrl && persisted?.userId && !persisted?.userToken);
1190
- if (!hasClientConfig && !hasPendingConnection) {
1191
- this.jsonResponse(res, base);
1192
- return;
1193
- }
1194
-
1195
- try {
1196
- const status = await getHubStatus(this.store, this.ctx.config);
1197
- const output = { ...base, connection: { ...base.connection, ...status } } as any;
1198
- if (status.user?.status === "pending") {
1199
- output.connection.pendingApproval = true;
1200
- }
1201
- if (status.user?.status === "rejected") {
1202
- output.connection.rejected = true;
1203
- }
1204
- if (status.connected && status.hubUrl) {
1205
- try {
1206
- const info = await fetch(`${status.hubUrl}/api/v1/hub/info`).then((r) => (r.ok ? r.json() : null)).catch(() => null) as any;
1207
- output.connection.teamName = info?.teamName ?? null;
1208
- output.connection.apiVersion = info?.apiVersion ?? null;
1209
- } catch {}
1210
- } else if (status.hubUrl) {
1211
- try {
1212
- const info = await fetch(`${status.hubUrl}/api/v1/hub/info`).then((r) => (r.ok ? r.json() : null)).catch(() => null) as any;
1213
- output.connection.teamName = info?.teamName ?? null;
1214
- } catch {}
1215
- }
1216
- output.admin.canManageUsers = status.connected && status.user?.role === "admin";
1217
- output.admin.rejectSupported = output.admin.canManageUsers;
1218
- this.jsonResponse(res, output);
1219
- } catch (err) {
1220
- this.jsonResponse(res, { ...base, error: String(err) });
1221
- }
1222
- }
1223
-
1224
- private async serveSharingPendingUsers(res: http.ServerResponse): Promise<void> {
1225
- if (!this.ctx) return this.jsonResponse(res, { users: [], error: "sharing_unavailable" });
1226
- try {
1227
- const hub = this.resolveHubConnection();
1228
- if (!hub) return this.jsonResponse(res, { users: [], error: "not_configured" });
1229
- const data = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/pending-users", { method: "GET" }) as any;
1230
- this.jsonResponse(res, { users: Array.isArray(data?.users) ? data.users : [] });
1231
- } catch (err) {
1232
- this.jsonResponse(res, { users: [], error: String(err) });
1233
- }
1234
- }
1235
-
1236
- private handleSharingApproveUser(req: http.IncomingMessage, res: http.ServerResponse): void {
1237
- this.readBody(req, async (body) => {
1238
- if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
1239
- try {
1240
- const parsed = JSON.parse(body || "{}");
1241
- const hub = this.resolveHubConnection();
1242
- if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
1243
- const result = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/approve-user", {
1244
- method: "POST",
1245
- body: JSON.stringify({ userId: parsed.userId, username: parsed.username }),
1246
- });
1247
- this.jsonResponse(res, { ok: true, result });
1248
- } catch (err) {
1249
- this.jsonResponse(res, { ok: false, error: String(err) });
1250
- }
1251
- });
1252
- }
1253
-
1254
- private handleSharingRejectUser(req: http.IncomingMessage, res: http.ServerResponse): void {
1255
- this.readBody(req, async (body) => {
1256
- if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
1257
- try {
1258
- const parsed = JSON.parse(body || "{}");
1259
- const hub = this.resolveHubConnection();
1260
- if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
1261
- const result = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/reject-user", {
1262
- method: "POST",
1263
- body: JSON.stringify({ userId: parsed.userId }),
1264
- });
1265
- this.jsonResponse(res, { ok: true, result });
1266
- } catch (err) {
1267
- this.jsonResponse(res, { ok: false, error: String(err) });
1268
- }
1269
- });
1270
- }
1271
-
1272
- private handleRetryJoin(req: http.IncomingMessage, res: http.ServerResponse): void {
1273
- this.readBody(req, async (_body) => {
1274
- if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
1275
- const sharing = this.ctx.config.sharing;
1276
- if (!sharing?.enabled || sharing.role !== "client") {
1277
- return this.jsonResponse(res, { ok: false, error: "not_in_client_mode" });
1278
- }
1279
- const hubAddress = sharing.client?.hubAddress ?? "";
1280
- const teamToken = sharing.client?.teamToken ?? "";
1281
- if (!hubAddress || !teamToken) {
1282
- return this.jsonResponse(res, { ok: false, error: "missing_hub_address_or_team_token" });
1283
- }
1284
- try {
1285
- const hubUrl = normalizeHubUrl(hubAddress);
1286
- const os = await import("os");
1287
- const username = os.userInfo().username || "user";
1288
- const hostname = os.hostname() || "unknown";
1289
- const result = await hubRequestJson(hubUrl, "", "/api/v1/hub/join", {
1290
- method: "POST",
1291
- body: JSON.stringify({ teamToken, username, deviceName: hostname }),
1292
- }) as any;
1293
- this.store.setClientHubConnection({
1294
- hubUrl,
1295
- userId: String(result.userId || ""),
1296
- username,
1297
- userToken: result.userToken || "",
1298
- role: "member",
1299
- connectedAt: Date.now(),
1300
- });
1301
- this.jsonResponse(res, { ok: true, status: result.status || "pending" });
1302
- } catch (err) {
1303
- this.jsonResponse(res, { ok: false, error: String(err) });
1304
- }
1305
- });
1306
- }
1307
-
1308
- private async serveSharingMemoryList(res: http.ServerResponse, url: URL): Promise<void> {
1309
- if (!this.ctx) return this.jsonResponse(res, { memories: [], error: "sharing_unavailable" });
1310
- try {
1311
- const limit = Number(url.searchParams.get("limit") || 40);
1312
- const data = await hubListMemories(this.store, this.ctx, { limit });
1313
- this.jsonResponse(res, { memories: Array.isArray(data?.memories) ? data.memories : [] });
1314
- } catch (err) {
1315
- this.jsonResponse(res, { memories: [], error: String(err) });
1316
- }
1317
- }
1318
-
1319
- private async serveSharingTaskList(res: http.ServerResponse, url: URL): Promise<void> {
1320
- if (!this.ctx) return this.jsonResponse(res, { tasks: [], error: "sharing_unavailable" });
1321
- try {
1322
- const limit = Number(url.searchParams.get("limit") || 40);
1323
- const data = await hubListTasks(this.store, this.ctx, { limit });
1324
- this.jsonResponse(res, { tasks: Array.isArray(data?.tasks) ? data.tasks : [] });
1325
- } catch (err) {
1326
- this.jsonResponse(res, { tasks: [], error: String(err) });
1327
- }
1328
- }
1329
-
1330
- private async serveSharingSkillList(res: http.ServerResponse, url: URL): Promise<void> {
1331
- if (!this.ctx) return this.jsonResponse(res, { skills: [], error: "sharing_unavailable" });
1332
- try {
1333
- const limit = Number(url.searchParams.get("limit") || 40);
1334
- const data = await hubListSkills(this.store, this.ctx, { limit });
1335
- this.jsonResponse(res, { skills: Array.isArray(data?.skills) ? data.skills : [] });
1336
- } catch (err) {
1337
- this.jsonResponse(res, { skills: [], error: String(err) });
1338
- }
1339
- }
1340
-
1341
- private handleSharingMemorySearch(req: http.IncomingMessage, res: http.ServerResponse): void {
1342
- this.readBody(req, async (body) => {
1343
- if (!this.ctx) return this.jsonResponse(res, { local: { hits: [], meta: {} }, hub: { hits: [], meta: { totalCandidates: 0, searchedGroups: [], includedPublic: false } }, error: "sharing_unavailable" });
1344
- const emptyHub = { hits: [], meta: { totalCandidates: 0, searchedGroups: [], includedPublic: false } };
1345
- try {
1346
- const parsed = JSON.parse(body || "{}");
1347
- const query = String(parsed.query || "");
1348
- const role = typeof parsed.role === "string" ? parsed.role : undefined;
1349
- const maxResults = typeof parsed.maxResults === "number" ? parsed.maxResults : 10;
1350
- const scope = parsed.scope === "group" || parsed.scope === "all" ? parsed.scope : "local";
1351
- const local = this.searchLocalViewerMemories(query, { role, maxResults });
1352
- if (scope === "local") {
1353
- return this.jsonResponse(res, { local: { hits: local.hits, meta: local.meta }, hub: emptyHub });
1354
- }
1355
- try {
1356
- const hub = await hubSearchMemories(this.store, this.ctx, { query, maxResults, scope });
1357
- this.jsonResponse(res, { local: { hits: local.hits, meta: local.meta }, hub });
1358
- } catch (err) {
1359
- this.jsonResponse(res, { local: { hits: local.hits, meta: local.meta }, hub: emptyHub, error: String(err) });
1360
- }
1361
- } catch (err) {
1362
- this.jsonResponse(res, { local: { hits: [], meta: {} }, hub: emptyHub, error: String(err) });
1363
- }
1364
- });
1365
- }
1366
-
1367
- private handleSharingMemoryDetail(req: http.IncomingMessage, res: http.ServerResponse): void {
1368
- this.readBody(req, async (body) => {
1369
- if (!this.ctx) return this.jsonResponse(res, { error: "sharing_unavailable" });
1370
- try {
1371
- const parsed = JSON.parse(body || "{}");
1372
- const detail = await hubGetMemoryDetail(this.store, this.ctx, { remoteHitId: String(parsed.remoteHitId || "") });
1373
- this.jsonResponse(res, detail);
1374
- } catch (err) {
1375
- this.jsonResponse(res, { error: String(err) });
1376
- }
1377
- });
1378
- }
1379
-
1380
- private async serveSharingSkillSearch(res: http.ServerResponse, url: URL): Promise<void> {
1381
- if (!this.ctx) return this.jsonResponse(res, { local: { hits: [] }, hub: { hits: [] }, error: "sharing_unavailable" });
1382
- try {
1383
- const query = String(url.searchParams.get("query") || "");
1384
- const scope = url.searchParams.get("scope") === "group" || url.searchParams.get("scope") === "all" ? url.searchParams.get("scope")! : "local";
1385
- const recall = new RecallEngine(this.store, this.embedder, this.ctx);
1386
- const localHits = await recall.searchSkills(query, "mix" as any, "agent:main");
1387
- if (scope === "local") {
1388
- return this.jsonResponse(res, { local: { hits: localHits }, hub: { hits: [] } });
1389
- }
1390
- try {
1391
- const hub = await hubSearchSkills(this.store, this.ctx, { query, maxResults: Number(url.searchParams.get("maxResults") || 20) });
1392
- this.jsonResponse(res, { local: { hits: localHits }, hub });
1393
- } catch (err) {
1394
- this.jsonResponse(res, { local: { hits: localHits }, hub: { hits: [] }, error: String(err) });
1395
- }
1396
- } catch (err) {
1397
- this.jsonResponse(res, { local: { hits: [] }, hub: { hits: [] }, error: String(err) });
1398
- }
1399
- }
1400
-
1401
- private searchLocalViewerMemories(query: string, options?: { role?: string; maxResults?: number }): { hits: any[]; meta: Record<string, unknown> } {
1402
- const db = (this.store as any).db;
1403
- const role = options?.role;
1404
- const maxResults = options?.maxResults ?? 10;
1405
- const params: any[] = [];
1406
- let rows: any[] = [];
1407
- try {
1408
- let sql = "SELECT c.* FROM chunks_fts f JOIN chunks c ON f.rowid = c.rowid WHERE chunks_fts MATCH ?";
1409
- params.push(query);
1410
- if (role) {
1411
- sql += " AND c.role = ?";
1412
- params.push(role);
1413
- }
1414
- sql += " ORDER BY rank LIMIT ?";
1415
- params.push(maxResults);
1416
- rows = db.prepare(sql).all(...params);
1417
- } catch {
1418
- const likeParams: any[] = [`%${query}%`, `%${query}%`];
1419
- let sql = "SELECT * FROM chunks WHERE (content LIKE ? OR summary LIKE ?)";
1420
- if (role) {
1421
- sql += " AND role = ?";
1422
- likeParams.push(role);
1423
- }
1424
- sql += " ORDER BY created_at DESC LIMIT ?";
1425
- likeParams.push(maxResults);
1426
- rows = db.prepare(sql).all(...likeParams);
1427
- }
1428
- const hits = rows.map((row: any, idx: number) => ({
1429
- id: row.id,
1430
- summary: row.summary || row.content?.slice(0, 120) || "",
1431
- excerpt: row.content || "",
1432
- score: Math.max(0.3, 1 - idx * 0.1),
1433
- role: row.role,
1434
- ref: { sessionKey: row.session_key, chunkId: row.id, turnId: row.turn_id, seq: row.seq },
1435
- taskId: row.task_id ?? null,
1436
- skillId: row.skill_id ?? null,
1437
- }));
1438
- return { hits, meta: { total: hits.length, usedMaxResults: maxResults } };
1439
- }
1440
-
1441
- private handleSharingTaskShare(req: http.IncomingMessage, res: http.ServerResponse): void {
1442
- this.readBody(req, async (body) => {
1443
- if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
1444
- try {
1445
- const parsed = JSON.parse(body || "{}");
1446
- const taskId = String(parsed.taskId || "");
1447
- const visibility = "public";
1448
- const groupId: string | undefined = undefined;
1449
- const task = this.store.getTask(taskId);
1450
- if (!task) return this.jsonResponse(res, { ok: false, error: "task_not_found" });
1451
- const chunks = this.store.getChunksByTask(taskId);
1452
- if (chunks.length === 0) return this.jsonResponse(res, { ok: false, error: "no_chunks" });
1453
- const hubClient = await this.resolveHubClientAware();
1454
- const response = await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/share", {
1455
- method: "POST",
1456
- body: JSON.stringify({
1457
- task: {
1458
- id: task.id,
1459
- sourceTaskId: task.id,
1460
- title: task.title,
1461
- summary: task.summary,
1462
- groupId: null,
1463
- visibility,
1464
- createdAt: task.startedAt ?? Date.now(),
1465
- updatedAt: task.updatedAt ?? Date.now(),
1466
- },
1467
- chunks: chunks.map((chunk) => ({
1468
- id: chunk.id,
1469
- hubTaskId: task.id,
1470
- sourceTaskId: task.id,
1471
- sourceChunkId: chunk.id,
1472
- role: chunk.role,
1473
- content: chunk.content,
1474
- summary: chunk.summary,
1475
- kind: chunk.kind,
1476
- createdAt: chunk.createdAt,
1477
- })),
1478
- }),
1479
- });
1480
- const hubUserId = hubClient.userId;
1481
- if (hubUserId) {
1482
- this.store.upsertHubTask({
1483
- id: task.id,
1484
- sourceTaskId: task.id,
1485
- sourceUserId: hubUserId,
1486
- title: task.title,
1487
- summary: task.summary,
1488
- groupId: null,
1489
- visibility,
1490
- createdAt: task.startedAt ?? Date.now(),
1491
- updatedAt: task.updatedAt ?? Date.now(),
1492
- });
1493
- }
1494
- this.jsonResponse(res, { ok: true, taskId, visibility, response });
1495
- } catch (err) {
1496
- this.jsonResponse(res, { ok: false, error: String(err) });
1497
- }
1498
- });
1499
- }
1500
-
1501
- private handleSharingTaskUnshare(req: http.IncomingMessage, res: http.ServerResponse): void {
1502
- this.readBody(req, async (body) => {
1503
- if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
1504
- try {
1505
- const parsed = JSON.parse(body || "{}");
1506
- const taskId = String(parsed.taskId || "");
1507
- const task = this.store.getTask(taskId);
1508
- if (!task) return this.jsonResponse(res, { ok: false, error: "task_not_found" });
1509
- const hubClient = await this.resolveHubClientAware();
1510
- await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/unshare", {
1511
- method: "POST",
1512
- body: JSON.stringify({ sourceTaskId: task.id }),
1513
- });
1514
- const hubUserId = hubClient.userId;
1515
- if (hubUserId) this.store.deleteHubTaskBySource(hubUserId, task.id);
1516
- this.jsonResponse(res, { ok: true, taskId });
1517
- } catch (err) {
1518
- this.jsonResponse(res, { ok: false, error: String(err) });
1519
- }
1520
- });
1521
- }
1522
-
1523
- private handleSharingMemoryShare(req: http.IncomingMessage, res: http.ServerResponse): void {
1524
- this.readBody(req, async (body) => {
1525
- if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
1526
- try {
1527
- const parsed = JSON.parse(body || "{}");
1528
- const chunkId = String(parsed.chunkId || "");
1529
- const visibility = "public";
1530
- const groupId: string | undefined = undefined;
1531
- const db = (this.store as any).db;
1532
- const chunk = db.prepare("SELECT * FROM chunks WHERE id = ?").get(chunkId) as any;
1533
- if (!chunk) return this.jsonResponse(res, { ok: false, error: "memory_not_found" });
1534
- const hubClient = await this.resolveHubClientAware();
1535
- const response = await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/share", {
1536
- method: "POST",
1537
- body: JSON.stringify({
1538
- memory: {
1539
- sourceChunkId: chunk.id,
1540
- role: chunk.role,
1541
- content: chunk.content,
1542
- summary: chunk.summary,
1543
- kind: chunk.kind,
1544
- groupId: null,
1545
- visibility,
1546
- },
1547
- }),
1548
- });
1549
- const hubUserId = hubClient.userId;
1550
- if (hubUserId) {
1551
- const now = Date.now();
1552
- const existing = this.store.getHubMemoryBySource(hubUserId, chunk.id);
1553
- this.store.upsertHubMemory({
1554
- id: (response as any)?.memoryId ?? existing?.id ?? crypto.randomUUID(),
1555
- sourceChunkId: chunk.id,
1556
- sourceUserId: hubUserId,
1557
- role: chunk.role,
1558
- content: chunk.content,
1559
- summary: chunk.summary ?? "",
1560
- kind: chunk.kind,
1561
- groupId: null,
1562
- visibility,
1563
- createdAt: existing?.createdAt ?? now,
1564
- updatedAt: now,
1565
- });
1566
- }
1567
- this.jsonResponse(res, { ok: true, chunkId, visibility, response });
1568
- } catch (err) {
1569
- this.jsonResponse(res, { ok: false, error: String(err) });
1570
- }
1571
- });
1572
- }
1573
-
1574
- private handleSharingMemoryUnshare(req: http.IncomingMessage, res: http.ServerResponse): void {
1575
- this.readBody(req, async (body) => {
1576
- if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
1577
- try {
1578
- const parsed = JSON.parse(body || "{}");
1579
- const chunkId = String(parsed.chunkId || "");
1580
- const hubClient = await this.resolveHubClientAware();
1581
- await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/unshare", {
1582
- method: "POST",
1583
- body: JSON.stringify({ sourceChunkId: chunkId }),
1584
- });
1585
- const hubUserId = hubClient.userId;
1586
- if (hubUserId) this.store.deleteHubMemoryBySource(hubUserId, chunkId);
1587
- this.jsonResponse(res, { ok: true, chunkId });
1588
- } catch (err) {
1589
- this.jsonResponse(res, { ok: false, error: String(err) });
1590
- }
1591
- });
1592
- }
1593
-
1594
- private handleSharingSkillPull(req: http.IncomingMessage, res: http.ServerResponse): void {
1595
- this.readBody(req, async (body) => {
1596
- if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
1597
- try {
1598
- const parsed = JSON.parse(body || "{}");
1599
- const skillId = String(parsed.skillId || "");
1600
- const payload = await fetchHubSkillBundle(this.store, this.ctx, { skillId });
1601
- const restored = restoreSkillBundleFromHub(this.store, this.ctx, payload);
1602
- this.jsonResponse(res, { ok: true, pulled: true, hubSkillId: skillId, ...restored });
1603
- } catch (err) {
1604
- this.jsonResponse(res, { ok: false, error: String(err) });
1605
- }
1606
- });
1607
- }
1608
-
1609
- private handleSharingSkillShare(req: http.IncomingMessage, res: http.ServerResponse): void {
1610
- this.readBody(req, async (body) => {
1611
- if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
1612
- try {
1613
- const parsed = JSON.parse(body || "{}");
1614
- const skillId = String(parsed.skillId || "");
1615
- const visibility = "public";
1616
- const groupId: string | null = null;
1617
- const skill = this.store.getSkill(skillId);
1618
- if (!skill) return this.jsonResponse(res, { ok: false, error: "skill_not_found" });
1619
- const bundle = buildSkillBundleForHub(this.store, skillId);
1620
- const hubClient = await this.resolveHubClientAware();
1621
- const response = await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/publish", {
1622
- method: "POST",
1623
- body: JSON.stringify({
1624
- visibility,
1625
- groupId: null,
1626
- metadata: bundle.metadata,
1627
- bundle: bundle.bundle,
1628
- }),
1629
- });
1630
- const hubUserId = hubClient.userId;
1631
- if (hubUserId) {
1632
- const existing = this.store.getHubSkillBySource(hubUserId, skillId);
1633
- this.store.upsertHubSkill({
1634
- id: (response as any)?.skillId ?? existing?.id ?? crypto.randomUUID(),
1635
- sourceSkillId: skillId,
1636
- sourceUserId: hubUserId,
1637
- name: skill.name,
1638
- description: skill.description,
1639
- version: skill.version,
1640
- groupId: null,
1641
- visibility,
1642
- bundle: JSON.stringify(bundle.bundle),
1643
- qualityScore: skill.qualityScore,
1644
- createdAt: existing?.createdAt ?? Date.now(),
1645
- updatedAt: Date.now(),
1646
- });
1647
- }
1648
- this.jsonResponse(res, { ok: true, skillId, visibility, response });
1649
- } catch (err) {
1650
- this.jsonResponse(res, { ok: false, error: String(err) });
1651
- }
1652
- });
1653
- }
1654
-
1655
- private handleSharingSkillUnshare(req: http.IncomingMessage, res: http.ServerResponse): void {
1656
- this.readBody(req, async (body) => {
1657
- if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
1658
- try {
1659
- const parsed = JSON.parse(body || "{}");
1660
- const skillId = String(parsed.skillId || "");
1661
- const skill = this.store.getSkill(skillId);
1662
- if (!skill) return this.jsonResponse(res, { ok: false, error: "skill_not_found" });
1663
- const hubClient = await this.resolveHubClientAware();
1664
- await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/unpublish", {
1665
- method: "POST",
1666
- body: JSON.stringify({ sourceSkillId: skill.id }),
1667
- });
1668
- const hubUserId = hubClient.userId;
1669
- if (hubUserId) this.store.deleteHubSkillBySource(hubUserId, skill.id);
1670
- this.jsonResponse(res, { ok: true, skillId });
1671
- } catch (err) {
1672
- this.jsonResponse(res, { ok: false, error: String(err) });
1673
- }
1674
- });
1675
- }
1676
-
1677
- private resolveHubConnection(): { hubUrl: string; userToken: string } | null {
1678
- if (!this.ctx) return null;
1679
-
1680
- // Hub 模式:连接自己,用 bootstrap admin token
1681
- const sharing = this.ctx.config.sharing;
1682
- if (sharing?.role === "hub") {
1683
- const hubPort = sharing.hub?.port ?? 18800;
1684
- const hubUrl = `http://127.0.0.1:${hubPort}`;
1685
- try {
1686
- const authPath = path.join(this.dataDir, "hub-auth.json");
1687
- const authData = JSON.parse(fs.readFileSync(authPath, "utf8"));
1688
- const adminToken = authData?.bootstrapAdminToken;
1689
- if (adminToken) return { hubUrl, userToken: adminToken };
1690
- } catch {
1691
- // hub-auth.json 不存在或读取失败,fall through
1692
- }
1693
- }
1694
-
1695
- // Client 模式:用配置的 hubAddress + userToken
1696
- const conn = this.store.getClientHubConnection();
1697
- const hubUrl = conn?.hubUrl || this.ctx.config.sharing?.client?.hubAddress || "";
1698
- const userToken = conn?.userToken || this.ctx.config.sharing?.client?.userToken || "";
1699
- if (!hubUrl || !userToken) return null;
1700
- return { hubUrl: normalizeHubUrl(hubUrl), userToken };
1701
- }
1702
-
1703
- /** resolveHubClient 的 viewer 版本:hub 模式下使用 bootstrap admin 身份 */
1704
- private async resolveHubClientAware(): Promise<ResolvedHubClient> {
1705
- if (!this.ctx) throw new Error("sharing_unavailable");
1706
- const sharing = this.ctx.config.sharing;
1707
- if (sharing?.role === "hub") {
1708
- const hub = this.resolveHubConnection();
1709
- if (hub) {
1710
- const me = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/me", { method: "GET" }) as any;
1711
- return {
1712
- hubUrl: hub.hubUrl,
1713
- userToken: hub.userToken,
1714
- userId: String(me.id),
1715
- username: String(me.username ?? "hub-admin"),
1716
- role: String(me.role ?? "admin"),
1717
- };
1718
- }
1719
- }
1720
- return resolveHubClient(this.store, this.ctx);
1721
- }
1722
-
1723
- private extractGroupId(path: string): string {
1724
- const m = path.match(/\/api\/sharing\/groups\/([^/]+)/);
1725
- return m ? decodeURIComponent(m[1]) : "";
1726
- }
1727
-
1728
- private async serveSharingGroups(res: http.ServerResponse): Promise<void> {
1729
- const hub = this.resolveHubConnection();
1730
- if (!hub) return this.jsonResponse(res, { groups: [], error: "not_configured" });
1731
- try {
1732
- const data = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/groups", { method: "GET" }) as any;
1733
- this.jsonResponse(res, { groups: Array.isArray(data?.groups) ? data.groups : [] });
1734
- } catch (err) {
1735
- this.jsonResponse(res, { groups: [], error: String(err) });
1736
- }
1737
- }
1738
-
1739
- private handleSharingGroupCreate(req: http.IncomingMessage, res: http.ServerResponse): void {
1740
- this.readBody(req, async (body) => {
1741
- const hub = this.resolveHubConnection();
1742
- if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
1743
- try {
1744
- const parsed = JSON.parse(body || "{}");
1745
- const data = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/groups", {
1746
- method: "POST",
1747
- body: JSON.stringify({ name: parsed.name, description: parsed.description }),
1748
- }) as any;
1749
- this.jsonResponse(res, { ok: true, ...data });
1750
- } catch (err) {
1751
- this.jsonResponse(res, { ok: false, error: String(err) });
1752
- }
1753
- });
1754
- }
1755
-
1756
- private handleSharingGroupUpdate(req: http.IncomingMessage, res: http.ServerResponse, p: string): void {
1757
- this.readBody(req, async (body) => {
1758
- const hub = this.resolveHubConnection();
1759
- if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
1760
- const groupId = this.extractGroupId(p);
1761
- try {
1762
- const parsed = JSON.parse(body || "{}");
1763
- await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}`, {
1764
- method: "PUT",
1765
- body: JSON.stringify({ name: parsed.name, description: parsed.description }),
1766
- });
1767
- this.jsonResponse(res, { ok: true });
1768
- } catch (err) {
1769
- this.jsonResponse(res, { ok: false, error: String(err) });
1770
- }
1771
- });
1772
- }
1773
-
1774
- private async handleSharingGroupDelete(res: http.ServerResponse, p: string): Promise<void> {
1775
- const hub = this.resolveHubConnection();
1776
- if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
1777
- const groupId = this.extractGroupId(p);
1778
- try {
1779
- await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}`, { method: "DELETE" });
1780
- this.jsonResponse(res, { ok: true });
1781
- } catch (err) {
1782
- this.jsonResponse(res, { ok: false, error: String(err) });
1783
- }
1784
- }
1785
-
1786
- private async serveSharingGroupMembers(res: http.ServerResponse, p: string): Promise<void> {
1787
- const hub = this.resolveHubConnection();
1788
- if (!hub) return this.jsonResponse(res, { members: [], error: "not_configured" });
1789
- const groupId = this.extractGroupId(p);
1790
- try {
1791
- const data = await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}`, { method: "GET" }) as any;
1792
- this.jsonResponse(res, { members: Array.isArray(data?.members) ? data.members : [] });
1793
- } catch (err) {
1794
- this.jsonResponse(res, { members: [], error: String(err) });
1795
- }
1796
- }
1797
-
1798
- private handleSharingGroupAddMember(req: http.IncomingMessage, res: http.ServerResponse, p: string): void {
1799
- this.readBody(req, async (body) => {
1800
- const hub = this.resolveHubConnection();
1801
- if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
1802
- const groupId = this.extractGroupId(p);
1803
- try {
1804
- const parsed = JSON.parse(body || "{}");
1805
- await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}/members`, {
1806
- method: "POST",
1807
- body: JSON.stringify({ userId: parsed.userId }),
1808
- });
1809
- this.jsonResponse(res, { ok: true });
1810
- } catch (err) {
1811
- this.jsonResponse(res, { ok: false, error: String(err) });
1812
- }
1813
- });
1814
- }
1815
-
1816
- private handleSharingGroupRemoveMember(req: http.IncomingMessage, res: http.ServerResponse, p: string): void {
1817
- this.readBody(req, async (body) => {
1818
- const hub = this.resolveHubConnection();
1819
- if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
1820
- const groupId = this.extractGroupId(p);
1821
- try {
1822
- const parsed = JSON.parse(body || "{}");
1823
- await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/groups/${encodeURIComponent(groupId)}/members`, {
1824
- method: "DELETE",
1825
- body: JSON.stringify({ userId: parsed.userId }),
1826
- });
1827
- this.jsonResponse(res, { ok: true });
1828
- } catch (err) {
1829
- this.jsonResponse(res, { ok: false, error: String(err) });
1830
- }
1831
- });
1832
- }
1833
-
1834
- private async serveSharingUsers(res: http.ServerResponse): Promise<void> {
1835
- const hub = this.resolveHubConnection();
1836
- if (!hub) return this.jsonResponse(res, { users: [], error: "not_configured" });
1837
- try {
1838
- const data = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/users", { method: "GET" }) as any;
1839
- this.jsonResponse(res, { users: Array.isArray(data?.users) ? data.users : [] });
1840
- } catch (err) {
1841
- this.jsonResponse(res, { users: [], error: String(err) });
1842
- }
1843
- }
1844
-
1845
- // ─── Admin management endpoints (Hub-side data) ───
1846
-
1847
- private async serveAdminSharedTasks(res: http.ServerResponse): Promise<void> {
1848
- const hub = this.resolveHubConnection();
1849
- if (!hub) return this.jsonResponse(res, { tasks: [], error: "not_configured" });
1850
- try {
1851
- const data = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/shared-tasks", { method: "GET" }) as any;
1852
- this.jsonResponse(res, { tasks: Array.isArray(data?.tasks) ? data.tasks : [] });
1853
- } catch (err) {
1854
- this.jsonResponse(res, { tasks: [], error: String(err) });
1855
- }
1856
- }
1857
-
1858
- private async handleAdminDeleteTask(res: http.ServerResponse, p: string): Promise<void> {
1859
- const hub = this.resolveHubConnection();
1860
- if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
1861
- const taskId = decodeURIComponent(p.replace("/api/admin/shared-tasks/", ""));
1862
- try {
1863
- await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/admin/shared-tasks/${encodeURIComponent(taskId)}`, { method: "DELETE" });
1864
- this.jsonResponse(res, { ok: true });
1865
- } catch (err) {
1866
- this.jsonResponse(res, { ok: false, error: String(err) });
1867
- }
1868
- }
1869
-
1870
- private async serveAdminSharedSkills(res: http.ServerResponse): Promise<void> {
1871
- const hub = this.resolveHubConnection();
1872
- if (!hub) return this.jsonResponse(res, { skills: [], error: "not_configured" });
1873
- try {
1874
- const data = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/shared-skills", { method: "GET" }) as any;
1875
- this.jsonResponse(res, { skills: Array.isArray(data?.skills) ? data.skills : [] });
1876
- } catch (err) {
1877
- this.jsonResponse(res, { skills: [], error: String(err) });
1878
- }
1879
- }
1880
-
1881
- private async handleAdminDeleteSkill(res: http.ServerResponse, p: string): Promise<void> {
1882
- const hub = this.resolveHubConnection();
1883
- if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
1884
- const skillId = decodeURIComponent(p.replace("/api/admin/shared-skills/", ""));
1885
- try {
1886
- await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/admin/shared-skills/${encodeURIComponent(skillId)}`, { method: "DELETE" });
1887
- this.jsonResponse(res, { ok: true });
1888
- } catch (err) {
1889
- this.jsonResponse(res, { ok: false, error: String(err) });
1890
- }
1891
- }
1892
-
1893
- private async serveAdminSharedMemories(res: http.ServerResponse): Promise<void> {
1894
- const hub = this.resolveHubConnection();
1895
- if (!hub) return this.jsonResponse(res, { memories: [], error: "not_configured" });
1896
- try {
1897
- const data = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/shared-memories", { method: "GET" }) as any;
1898
- this.jsonResponse(res, { memories: Array.isArray(data?.memories) ? data.memories : [] });
1899
- } catch (err) {
1900
- this.jsonResponse(res, { memories: [], error: String(err) });
1901
- }
1902
- }
1903
-
1904
- private async handleAdminDeleteMemory(res: http.ServerResponse, p: string): Promise<void> {
1905
- const hub = this.resolveHubConnection();
1906
- if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
1907
- const memoryId = decodeURIComponent(p.replace("/api/admin/shared-memories/", ""));
1908
- try {
1909
- await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/admin/shared-memories/${encodeURIComponent(memoryId)}`, { method: "DELETE" });
1910
- this.jsonResponse(res, { ok: true });
1911
- } catch (err) {
1912
- this.jsonResponse(res, { ok: false, error: String(err) });
1913
- }
1914
- }
1915
-
1916
- private getLocalIPs(): string[] {
1917
- const nets = os.networkInterfaces();
1918
- const ips: string[] = [];
1919
- for (const name of Object.keys(nets)) {
1920
- for (const net of nets[name] ?? []) {
1921
- if (net.family === "IPv4" && !net.internal) {
1922
- ips.push(net.address);
1923
- }
1924
- }
1925
- }
1926
- return ips;
1927
- }
1928
-
1929
- private serveLocalIPs(res: http.ServerResponse): void {
1930
- const ips = this.getLocalIPs();
1931
- res.writeHead(200, { "Content-Type": "application/json" });
1932
- res.end(JSON.stringify({ ips }));
995
+ const ocHome = process.env.OPENCLAW_STATE_DIR || path.join(home, ".openclaw");
996
+ return path.join(ocHome, "openclaw.json");
1933
997
  }
1934
998
 
1935
999
  private serveConfig(res: http.ServerResponse): void {
@@ -1952,9 +1016,7 @@ export class ViewerServer {
1952
1016
  ?? entries["memos-lite-openclaw-plugin"]
1953
1017
  ?? entries["memos-lite"]
1954
1018
  ?? {};
1955
- if ((pluginEntry as any).viewerPort != null) {
1956
- result.viewerPort = (pluginEntry as any).viewerPort;
1957
- } else if (topEntry.viewerPort) {
1019
+ if (pluginEntry.viewerPort == null && topEntry.viewerPort) {
1958
1020
  result.viewerPort = topEntry.viewerPort;
1959
1021
  }
1960
1022
  this.jsonResponse(res, result);
@@ -1993,35 +1055,6 @@ export class ViewerServer {
1993
1055
  if (newCfg.skillEvolution) config.skillEvolution = newCfg.skillEvolution;
1994
1056
  if (newCfg.viewerPort) config.viewerPort = newCfg.viewerPort;
1995
1057
  if (newCfg.telemetry !== undefined) config.telemetry = newCfg.telemetry;
1996
- if (newCfg.sharing !== undefined) {
1997
- const existing = (config.sharing as Record<string, unknown>) || {};
1998
- const merged = { ...existing, ...newCfg.sharing };
1999
- if (newCfg.sharing.capabilities && existing.capabilities) {
2000
- merged.capabilities = { ...(existing.capabilities as Record<string, unknown>), ...newCfg.sharing.capabilities };
2001
- }
2002
- if (merged.role === "client" && merged.client) {
2003
- const clientCfg = merged.client as Record<string, unknown>;
2004
- const addr = String(clientCfg.hubAddress || "");
2005
- if (addr) {
2006
- const localIPs = this.getLocalIPs();
2007
- localIPs.push("127.0.0.1", "localhost", "0.0.0.0");
2008
- try {
2009
- const u = new URL(addr.startsWith("http") ? addr : `http://${addr}`);
2010
- if (localIPs.includes(u.hostname)) {
2011
- res.writeHead(400, { "Content-Type": "application/json" });
2012
- res.end(JSON.stringify({ error: "cannot_join_self" }));
2013
- return;
2014
- }
2015
- } catch {}
2016
- }
2017
- }
2018
- if (merged.role === "hub") {
2019
- merged.client = { hubAddress: "", userToken: "", teamToken: "" };
2020
- } else if (merged.role === "client") {
2021
- merged.hub = { port: 18800, teamName: "", teamToken: "" };
2022
- }
2023
- config.sharing = merged;
2024
- }
2025
1058
 
2026
1059
  fs.mkdirSync(path.dirname(cfgPath), { recursive: true });
2027
1060
  fs.writeFileSync(cfgPath, JSON.stringify(raw, null, 2), "utf-8");
@@ -2035,88 +1068,6 @@ export class ViewerServer {
2035
1068
  });
2036
1069
  }
2037
1070
 
2038
- private handleUpdateUsername(req: http.IncomingMessage, res: http.ServerResponse): void {
2039
- this.readBody(req, async (body) => {
2040
- if (!this.ctx) return this.jsonResponse(res, { error: "sharing_unavailable" });
2041
- try {
2042
- const { username } = JSON.parse(body || "{}");
2043
- if (!username || typeof username !== "string" || username.trim().length < 2 || username.trim().length > 32) {
2044
- return this.jsonResponse(res, { error: "invalid_username" }, 400);
2045
- }
2046
- const trimmed = username.trim();
2047
- const hubClient = await this.resolveHubClientAware();
2048
- const result = await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/me/update-profile", {
2049
- method: "POST",
2050
- body: JSON.stringify({ username: trimmed }),
2051
- }) as any;
2052
- if (result.ok && result.userToken) {
2053
- const sharing = this.ctx.config.sharing;
2054
- if (sharing?.role === "hub") {
2055
- try {
2056
- const authPath = path.join(this.dataDir, "hub-auth.json");
2057
- const authData = JSON.parse(fs.readFileSync(authPath, "utf8"));
2058
- authData.bootstrapAdminToken = result.userToken;
2059
- fs.writeFileSync(authPath, JSON.stringify(authData, null, 2), "utf-8");
2060
- this.log.info("hub-auth.json updated with new admin token after username change");
2061
- } catch (e) {
2062
- this.log.warn(`Failed to update hub-auth.json: ${e}`);
2063
- }
2064
- } else {
2065
- const persisted = this.store.getClientHubConnection();
2066
- if (persisted) {
2067
- this.store.setClientHubConnection({
2068
- ...persisted,
2069
- username: result.username,
2070
- userToken: result.userToken,
2071
- });
2072
- }
2073
- }
2074
- }
2075
- this.jsonResponse(res, result);
2076
- } catch (err: any) {
2077
- const msg = String(err?.message || err);
2078
- if (msg.includes("409") || msg.includes("username_taken")) {
2079
- return this.jsonResponse(res, { error: "username_taken" }, 409);
2080
- }
2081
- this.jsonResponse(res, { error: msg }, 500);
2082
- }
2083
- });
2084
- }
2085
-
2086
- private handleTestHubConnection(req: http.IncomingMessage, res: http.ServerResponse): void {
2087
- this.readBody(req, async (body) => {
2088
- try {
2089
- const { hubUrl } = JSON.parse(body);
2090
- if (!hubUrl) { this.jsonResponse(res, { ok: false, error: "hubUrl is required" }); return; }
2091
- try {
2092
- const localIPs = this.getLocalIPs();
2093
- localIPs.push("127.0.0.1", "localhost", "0.0.0.0");
2094
- const parsed = new URL(hubUrl.startsWith("http") ? hubUrl : `http://${hubUrl}`);
2095
- if (localIPs.includes(parsed.hostname)) {
2096
- this.jsonResponse(res, { ok: false, error: "cannot_join_self" });
2097
- return;
2098
- }
2099
- } catch {}
2100
- const url = hubUrl.replace(/\/+$/, "") + "/api/v1/hub/info";
2101
- const ctrl = new AbortController();
2102
- const timeout = setTimeout(() => ctrl.abort(), 8000);
2103
- try {
2104
- const r = await fetch(url, { signal: ctrl.signal });
2105
- clearTimeout(timeout);
2106
- if (!r.ok) { this.jsonResponse(res, { ok: false, error: `HTTP ${r.status}` }); return; }
2107
- const info = await r.json() as Record<string, unknown>;
2108
- this.jsonResponse(res, { ok: true, teamName: info.teamName || "", apiVersion: info.apiVersion || "" });
2109
- } catch (e: unknown) {
2110
- clearTimeout(timeout);
2111
- const msg = e instanceof Error ? e.message : String(e);
2112
- this.jsonResponse(res, { ok: false, error: msg.includes("abort") ? "Connection timeout (8s)" : msg });
2113
- }
2114
- } catch (e) {
2115
- this.jsonResponse(res, { ok: false, error: String(e) });
2116
- }
2117
- });
2118
- }
2119
-
2120
1071
  private handleTestModel(req: http.IncomingMessage, res: http.ServerResponse): void {
2121
1072
  this.readBody(req, async (body) => {
2122
1073
  try {
@@ -2488,7 +1439,7 @@ export class ViewerServer {
2488
1439
 
2489
1440
  private getOpenClawHome(): string {
2490
1441
  const home = process.env.HOME || process.env.USERPROFILE || "";
2491
- return path.join(home, ".openclaw");
1442
+ return process.env.OPENCLAW_STATE_DIR || path.join(home, ".openclaw");
2492
1443
  }
2493
1444
 
2494
1445
  private handleCleanupPolluted(res: http.ServerResponse): void {
@@ -3415,8 +2366,8 @@ export class ViewerServer {
3415
2366
  req.on("end", () => cb(body));
3416
2367
  }
3417
2368
 
3418
- private jsonResponse(res: http.ServerResponse, data: unknown, statusCode = 200): void {
3419
- res.writeHead(statusCode, { "Content-Type": "application/json; charset=utf-8" });
2369
+ private jsonResponse(res: http.ServerResponse, data: unknown): void {
2370
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
3420
2371
  res.end(JSON.stringify(data));
3421
2372
  }
3422
2373
  }