@memtensor/memos-local-openclaw-plugin 1.0.4-beta.6 → 1.0.4-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -22
- package/dist/capture/index.d.ts +1 -1
- package/dist/capture/index.d.ts.map +1 -1
- package/dist/capture/index.js +27 -7
- package/dist/capture/index.js.map +1 -1
- package/dist/client/connector.d.ts +29 -0
- package/dist/client/connector.d.ts.map +1 -0
- package/dist/client/connector.js +218 -0
- package/dist/client/connector.js.map +1 -0
- package/dist/client/hub.d.ts +61 -0
- package/dist/client/hub.d.ts.map +1 -0
- package/dist/client/hub.js +170 -0
- package/dist/client/hub.js.map +1 -0
- package/dist/client/skill-sync.d.ts +36 -0
- package/dist/client/skill-sync.d.ts.map +1 -0
- package/dist/client/skill-sync.js +226 -0
- package/dist/client/skill-sync.js.map +1 -0
- package/dist/config.d.ts +2 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +72 -3
- package/dist/config.js.map +1 -1
- package/dist/embedding/index.d.ts +4 -2
- package/dist/embedding/index.d.ts.map +1 -1
- package/dist/embedding/index.js +17 -1
- package/dist/embedding/index.js.map +1 -1
- package/dist/hub/auth.d.ts +19 -0
- package/dist/hub/auth.d.ts.map +1 -0
- package/dist/hub/auth.js +70 -0
- package/dist/hub/auth.js.map +1 -0
- package/dist/hub/server.d.ts +41 -0
- package/dist/hub/server.d.ts.map +1 -0
- package/dist/hub/server.js +767 -0
- package/dist/hub/server.js.map +1 -0
- package/dist/hub/user-manager.d.ts +31 -0
- package/dist/hub/user-manager.d.ts.map +1 -0
- package/dist/hub/user-manager.js +129 -0
- package/dist/hub/user-manager.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -4
- package/dist/index.js.map +1 -1
- package/dist/ingest/providers/index.d.ts +10 -2
- package/dist/ingest/providers/index.d.ts.map +1 -1
- package/dist/ingest/providers/index.js +209 -43
- package/dist/ingest/providers/index.js.map +1 -1
- package/dist/ingest/providers/openai.d.ts +1 -0
- package/dist/ingest/providers/openai.d.ts.map +1 -1
- package/dist/ingest/providers/openai.js +1 -0
- package/dist/ingest/providers/openai.js.map +1 -1
- package/dist/ingest/task-processor.js +1 -1
- package/dist/ingest/task-processor.js.map +1 -1
- package/dist/openclaw-api.d.ts +53 -0
- package/dist/openclaw-api.d.ts.map +1 -0
- package/dist/openclaw-api.js +189 -0
- package/dist/openclaw-api.js.map +1 -0
- package/dist/recall/engine.js +1 -1
- package/dist/recall/engine.js.map +1 -1
- package/dist/shared/llm-call.d.ts +4 -2
- package/dist/shared/llm-call.d.ts.map +1 -1
- package/dist/shared/llm-call.js +20 -81
- package/dist/shared/llm-call.js.map +1 -1
- package/dist/sharing/types.contract.d.ts +2 -0
- package/dist/sharing/types.contract.d.ts.map +1 -0
- package/dist/sharing/types.contract.js +3 -0
- package/dist/sharing/types.contract.js.map +1 -0
- package/dist/sharing/types.d.ts +80 -0
- package/dist/sharing/types.d.ts.map +1 -0
- package/dist/sharing/types.js +3 -0
- package/dist/sharing/types.js.map +1 -0
- package/dist/skill/evaluator.d.ts.map +1 -1
- package/dist/skill/evaluator.js +2 -2
- package/dist/skill/evaluator.js.map +1 -1
- package/dist/skill/evolver.d.ts +0 -2
- package/dist/skill/evolver.d.ts.map +1 -1
- package/dist/skill/evolver.js +0 -3
- package/dist/skill/evolver.js.map +1 -1
- package/dist/skill/generator.d.ts.map +1 -1
- package/dist/skill/generator.js +4 -4
- package/dist/skill/generator.js.map +1 -1
- package/dist/skill/upgrader.js +1 -1
- package/dist/skill/upgrader.js.map +1 -1
- package/dist/skill/validator.js +1 -1
- package/dist/skill/validator.js.map +1 -1
- package/dist/storage/ensure-binding.d.ts.map +1 -1
- package/dist/storage/ensure-binding.js +3 -1
- package/dist/storage/ensure-binding.js.map +1 -1
- package/dist/storage/sqlite.d.ts +329 -1
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +909 -4
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/telemetry.d.ts +5 -12
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +38 -135
- package/dist/telemetry.js.map +1 -1
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +3 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/memory-search.d.ts +5 -2
- package/dist/tools/memory-search.d.ts.map +1 -1
- package/dist/tools/memory-search.js +50 -7
- package/dist/tools/memory-search.js.map +1 -1
- package/dist/tools/network-memory-detail.d.ts +4 -0
- package/dist/tools/network-memory-detail.d.ts.map +1 -0
- package/dist/tools/network-memory-detail.js +34 -0
- package/dist/tools/network-memory-detail.js.map +1 -0
- package/dist/types.d.ts +49 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/viewer/html.d.ts.map +1 -1
- package/dist/viewer/html.js +3965 -459
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +51 -0
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +1564 -23
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +769 -67
- package/openclaw.plugin.json +2 -1
- package/package.json +4 -3
- package/scripts/postinstall.cjs +283 -46
- package/skill/memos-memory-guide/SKILL.md +82 -20
- package/src/capture/index.ts +27 -7
- package/src/client/connector.ts +212 -0
- package/src/client/hub.ts +207 -0
- package/src/client/skill-sync.ts +216 -0
- package/src/config.ts +94 -3
- package/src/embedding/index.ts +21 -1
- package/src/hub/auth.ts +78 -0
- package/src/hub/server.ts +754 -0
- package/src/hub/user-manager.ts +143 -0
- package/src/index.ts +13 -5
- package/src/ingest/providers/index.ts +246 -46
- package/src/ingest/providers/openai.ts +1 -1
- package/src/ingest/task-processor.ts +1 -1
- package/src/openclaw-api.ts +287 -0
- package/src/recall/engine.ts +1 -1
- package/src/shared/llm-call.ts +23 -95
- package/src/sharing/types.contract.ts +40 -0
- package/src/sharing/types.ts +102 -0
- package/src/skill/evaluator.ts +3 -2
- package/src/skill/evolver.ts +0 -5
- package/src/skill/generator.ts +6 -4
- package/src/skill/upgrader.ts +1 -1
- package/src/skill/validator.ts +1 -1
- package/src/storage/ensure-binding.ts +3 -1
- package/src/storage/sqlite.ts +1159 -4
- package/src/telemetry.ts +39 -152
- package/src/tools/index.ts +1 -0
- package/src/tools/memory-search.ts +58 -8
- package/src/tools/network-memory-detail.ts +34 -0
- package/src/types.ts +44 -2
- package/src/viewer/html.ts +3965 -459
- package/src/viewer/server.ts +1452 -25
package/src/viewer/server.ts
CHANGED
|
@@ -14,7 +14,11 @@ 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
|
|
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";
|
|
18
22
|
import { viewerHTML } from "./html";
|
|
19
23
|
import { v4 as uuid } from "uuid";
|
|
20
24
|
|
|
@@ -220,7 +224,12 @@ export class ViewerServer {
|
|
|
220
224
|
}
|
|
221
225
|
|
|
222
226
|
if (p === "/api/memories" && req.method === "GET") this.serveMemories(res, url);
|
|
223
|
-
else if (p === "/api/
|
|
227
|
+
else if (p === "/api/memories/share-local" && req.method === "POST") this.handleMemoryLocalShare(req, res);
|
|
228
|
+
else if (p === "/api/memories/unshare-local" && req.method === "POST") this.handleMemoryLocalUnshare(req, res);
|
|
229
|
+
else if (p.match(/^\/api\/memory\/[^/]+\/scope$/) && req.method === "PUT") this.handleMemoryScope(req, res, p);
|
|
230
|
+
else if (p.match(/^\/api\/task\/[^/]+\/scope$/) && req.method === "PUT") this.handleTaskScope(req, res, p);
|
|
231
|
+
else if (p.match(/^\/api\/skill\/[^/]+\/scope$/) && req.method === "PUT") this.handleSkillScope(req, res, p);
|
|
232
|
+
else if (p === "/api/stats") this.serveStats(res, url);
|
|
224
233
|
else if (p === "/api/metrics") this.serveMetrics(res, url);
|
|
225
234
|
else if (p === "/api/tool-metrics") this.serveToolMetrics(res, url);
|
|
226
235
|
else if (p === "/api/search") this.serveSearch(req, res, url);
|
|
@@ -243,6 +252,40 @@ export class ViewerServer {
|
|
|
243
252
|
else if (p === "/api/memories" && req.method === "DELETE") this.handleDeleteAll(res);
|
|
244
253
|
else if (p === "/api/logs" && req.method === "GET") this.serveLogs(res, url);
|
|
245
254
|
else if (p === "/api/log-tools" && req.method === "GET") this.serveLogTools(res);
|
|
255
|
+
else if (p === "/api/sharing/status" && req.method === "GET") this.serveSharingStatus(res);
|
|
256
|
+
else if (p === "/api/sharing/pending-users" && req.method === "GET") this.serveSharingPendingUsers(res);
|
|
257
|
+
else if (p === "/api/sharing/approve-user" && req.method === "POST") this.handleSharingApproveUser(req, res);
|
|
258
|
+
else if (p === "/api/sharing/reject-user" && req.method === "POST") this.handleSharingRejectUser(req, res);
|
|
259
|
+
else if (p === "/api/sharing/remove-user" && req.method === "POST") this.handleSharingRemoveUser(req, res);
|
|
260
|
+
else if (p === "/api/sharing/change-role" && req.method === "POST") this.handleSharingChangeRole(req, res);
|
|
261
|
+
else if (p === "/api/sharing/retry-join" && req.method === "POST") this.handleRetryJoin(req, res);
|
|
262
|
+
else if (p === "/api/sharing/search/memories" && req.method === "POST") this.handleSharingMemorySearch(req, res);
|
|
263
|
+
else if (p === "/api/sharing/memories/list" && req.method === "GET") this.serveSharingMemoryList(res, url);
|
|
264
|
+
else if (p === "/api/sharing/tasks/list" && req.method === "GET") this.serveSharingTaskList(res, url);
|
|
265
|
+
else if (p === "/api/sharing/skills/list" && req.method === "GET") this.serveSharingSkillList(res, url);
|
|
266
|
+
else if (p === "/api/sharing/memory-detail" && req.method === "POST") this.handleSharingMemoryDetail(req, res);
|
|
267
|
+
else if (p === "/api/sharing/search/skills" && req.method === "GET") this.serveSharingSkillSearch(res, url);
|
|
268
|
+
else if (p === "/api/sharing/tasks/share" && req.method === "POST") this.handleSharingTaskShare(req, res);
|
|
269
|
+
else if (p === "/api/sharing/tasks/unshare" && req.method === "POST") this.handleSharingTaskUnshare(req, res);
|
|
270
|
+
else if (p === "/api/sharing/update-username" && req.method === "POST") this.handleUpdateUsername(req, res);
|
|
271
|
+
else if (p === "/api/sharing/rename-user" && req.method === "POST") this.handleAdminRenameUser(req, res);
|
|
272
|
+
else if (p === "/api/sharing/test-hub" && req.method === "POST") this.handleTestHubConnection(req, res);
|
|
273
|
+
else if (p === "/api/sharing/memories/share" && req.method === "POST") this.handleSharingMemoryShare(req, res);
|
|
274
|
+
else if (p === "/api/sharing/memories/unshare" && req.method === "POST") this.handleSharingMemoryUnshare(req, res);
|
|
275
|
+
else if (p === "/api/sharing/skills/pull" && req.method === "POST") this.handleSharingSkillPull(req, res);
|
|
276
|
+
else if (p === "/api/sharing/skills/share" && req.method === "POST") this.handleSharingSkillShare(req, res);
|
|
277
|
+
else if (p === "/api/sharing/skills/unshare" && req.method === "POST") this.handleSharingSkillUnshare(req, res);
|
|
278
|
+
else if (p === "/api/sharing/users" && req.method === "GET") this.serveSharingUsers(res);
|
|
279
|
+
else if (p === "/api/sharing/notifications" && req.method === "GET") this.serveSharingNotifications(res, url);
|
|
280
|
+
else if (p === "/api/sharing/notifications/read" && req.method === "POST") this.handleSharingNotificationsRead(req, res);
|
|
281
|
+
else if (p === "/api/sharing/notifications/clear" && req.method === "POST") this.handleSharingNotificationsClear(req, res);
|
|
282
|
+
else if (p === "/api/admin/shared-tasks" && req.method === "GET") this.serveAdminSharedTasks(res);
|
|
283
|
+
else if (p.match(/^\/api\/admin\/shared-tasks\/[^/]+$/) && req.method === "DELETE") this.handleAdminDeleteTask(res, p);
|
|
284
|
+
else if (p === "/api/admin/shared-skills" && req.method === "GET") this.serveAdminSharedSkills(res);
|
|
285
|
+
else if (p.match(/^\/api\/admin\/shared-skills\/[^/]+$/) && req.method === "DELETE") this.handleAdminDeleteSkill(res, p);
|
|
286
|
+
else if (p === "/api/admin/shared-memories" && req.method === "GET") this.serveAdminSharedMemories(res);
|
|
287
|
+
else if (p.match(/^\/api\/admin\/shared-memories\/[^/]+$/) && req.method === "DELETE") this.handleAdminDeleteMemory(res, p);
|
|
288
|
+
else if (p === "/api/local-ips" && req.method === "GET") this.serveLocalIPs(res);
|
|
246
289
|
else if (p === "/api/config" && req.method === "GET") this.serveConfig(res);
|
|
247
290
|
else if (p === "/api/config" && req.method === "PUT") this.handleSaveConfig(req, res);
|
|
248
291
|
else if (p === "/api/test-model" && req.method === "POST") this.handleTestModel(req, res);
|
|
@@ -401,15 +444,34 @@ export class ViewerServer {
|
|
|
401
444
|
const totalRow = db.prepare("SELECT COUNT(*) as count FROM chunks" + where).get(...params) as any;
|
|
402
445
|
const rawMemories = db.prepare("SELECT * FROM chunks" + where + ` ORDER BY created_at ${sortBy} LIMIT ? OFFSET ?`).all(...params, limit, offset);
|
|
403
446
|
const findMergeSources = db.prepare("SELECT id, summary, role FROM chunks WHERE dedup_target = ? AND (dedup_status = 'merged' OR dedup_status = 'duplicate')");
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
447
|
+
|
|
448
|
+
const chunkIds = rawMemories.map((m: any) => m.id);
|
|
449
|
+
const sharingMap = new Map<string, { visibility: string; group_id: string | null }>();
|
|
450
|
+
const localShareMap = new Map<string, { original_owner: string; shared_at: number }>();
|
|
451
|
+
if (chunkIds.length > 0) {
|
|
452
|
+
try {
|
|
453
|
+
const placeholders = chunkIds.map(() => "?").join(",");
|
|
454
|
+
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 }>;
|
|
455
|
+
for (const r of sharedRows) sharingMap.set(r.source_chunk_id, r);
|
|
456
|
+
const localRows = db.prepare(`SELECT chunk_id, original_owner, shared_at FROM local_shared_memories WHERE chunk_id IN (${placeholders})`).all(...chunkIds) as Array<{ chunk_id: string; original_owner: string; shared_at: number }>;
|
|
457
|
+
for (const r of localRows) localShareMap.set(r.chunk_id, r);
|
|
458
|
+
} catch {
|
|
407
459
|
}
|
|
408
|
-
|
|
460
|
+
}
|
|
461
|
+
const memories = rawMemories.map((m: any) => {
|
|
462
|
+
const out: any = m.role === "user" && m.content ? { ...m, content: stripInboundMetadata(m.content) } : { ...m };
|
|
463
|
+
if (out.merge_count > 0) {
|
|
409
464
|
const sources = findMergeSources.all(m.id) as Array<{ id: string; summary: string; role: string }>;
|
|
410
|
-
|
|
465
|
+
out.merge_sources = sources;
|
|
411
466
|
}
|
|
412
|
-
|
|
467
|
+
const shared = sharingMap.get(m.id);
|
|
468
|
+
const localShared = localShareMap.get(m.id);
|
|
469
|
+
out.sharingVisibility = shared?.visibility ?? null;
|
|
470
|
+
out.sharingGroupId = shared?.group_id ?? null;
|
|
471
|
+
out.localSharing = out.owner === "public";
|
|
472
|
+
out.localSharingManaged = !!localShared;
|
|
473
|
+
out.localOriginalOwner = localShared?.original_owner ?? null;
|
|
474
|
+
return out;
|
|
413
475
|
});
|
|
414
476
|
|
|
415
477
|
this.store.recordViewerEvent("list");
|
|
@@ -426,7 +488,21 @@ export class ViewerServer {
|
|
|
426
488
|
}
|
|
427
489
|
|
|
428
490
|
private serveToolMetrics(res: http.ServerResponse, url: URL): void {
|
|
429
|
-
const
|
|
491
|
+
const fromParam = url.searchParams.get("from");
|
|
492
|
+
const toParam = url.searchParams.get("to");
|
|
493
|
+
if (fromParam) {
|
|
494
|
+
const fromMs = new Date(fromParam).getTime();
|
|
495
|
+
const toMs = toParam ? new Date(toParam).getTime() : Date.now();
|
|
496
|
+
if (isNaN(fromMs) || isNaN(toMs)) {
|
|
497
|
+
this.jsonResponse(res, { error: "Invalid date" }, 400);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
const diffMin = Math.max(10, Math.min(43200, Math.round((toMs - fromMs) / 60000)));
|
|
501
|
+
const data = this.store.getToolMetrics(diffMin, fromMs, toMs);
|
|
502
|
+
this.jsonResponse(res, data);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
const minutes = Math.min(43200, Math.max(10, Number(url.searchParams.get("minutes")) || 60));
|
|
430
506
|
const data = this.store.getToolMetrics(minutes);
|
|
431
507
|
this.jsonResponse(res, data);
|
|
432
508
|
}
|
|
@@ -440,7 +516,8 @@ export class ViewerServer {
|
|
|
440
516
|
|
|
441
517
|
const db = (this.store as any).db;
|
|
442
518
|
const items = tasks.map((t) => {
|
|
443
|
-
const meta = db.prepare("SELECT skill_status FROM tasks WHERE id = ?").get(t.id) as { skill_status: string | null } | undefined;
|
|
519
|
+
const meta = db.prepare("SELECT skill_status, owner FROM tasks WHERE id = ?").get(t.id) as { skill_status: string | null; owner: string | null } | undefined;
|
|
520
|
+
const sharedTask = db.prepare("SELECT visibility FROM hub_tasks WHERE source_task_id = ? ORDER BY updated_at DESC LIMIT 1").get(t.id) as { visibility: string } | undefined;
|
|
444
521
|
return {
|
|
445
522
|
id: t.id,
|
|
446
523
|
sessionKey: t.sessionKey,
|
|
@@ -451,6 +528,8 @@ export class ViewerServer {
|
|
|
451
528
|
endedAt: t.endedAt,
|
|
452
529
|
chunkCount: this.store.countChunksByTask(t.id),
|
|
453
530
|
skillStatus: meta?.skill_status ?? null,
|
|
531
|
+
owner: meta?.owner ?? "agent:main",
|
|
532
|
+
sharingVisibility: sharedTask?.visibility ?? null,
|
|
454
533
|
};
|
|
455
534
|
});
|
|
456
535
|
|
|
@@ -485,6 +564,7 @@ export class ViewerServer {
|
|
|
485
564
|
const db = (this.store as any).db;
|
|
486
565
|
const meta = db.prepare("SELECT skill_status, skill_reason FROM tasks WHERE id = ?").get(taskId) as
|
|
487
566
|
{ skill_status: string | null; skill_reason: string | null } | undefined;
|
|
567
|
+
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;
|
|
488
568
|
|
|
489
569
|
this.jsonResponse(res, {
|
|
490
570
|
id: task.id,
|
|
@@ -492,16 +572,20 @@ export class ViewerServer {
|
|
|
492
572
|
title: task.title,
|
|
493
573
|
summary: task.summary,
|
|
494
574
|
status: task.status,
|
|
575
|
+
owner: task.owner ?? "agent:main",
|
|
495
576
|
startedAt: task.startedAt,
|
|
496
577
|
endedAt: task.endedAt,
|
|
497
578
|
chunks: chunkItems,
|
|
498
579
|
skillStatus: meta?.skill_status ?? null,
|
|
499
580
|
skillReason: meta?.skill_reason ?? null,
|
|
500
581
|
skillLinks,
|
|
582
|
+
sharingVisibility: sharedTask?.visibility ?? null,
|
|
583
|
+
sharingGroupId: sharedTask?.group_id ?? null,
|
|
584
|
+
hubTaskId: sharedTask ? true : false,
|
|
501
585
|
});
|
|
502
586
|
}
|
|
503
587
|
|
|
504
|
-
private serveStats(res: http.ServerResponse): void {
|
|
588
|
+
private serveStats(res: http.ServerResponse, url?: URL): void {
|
|
505
589
|
const emptyStats = {
|
|
506
590
|
totalMemories: 0, totalSessions: 0, totalEmbeddings: 0, totalSkills: 0,
|
|
507
591
|
embeddingProvider: this.embedder?.provider ?? "none",
|
|
@@ -515,6 +599,8 @@ export class ViewerServer {
|
|
|
515
599
|
return;
|
|
516
600
|
}
|
|
517
601
|
|
|
602
|
+
const ownerFilter = url?.searchParams.get("owner") ?? "";
|
|
603
|
+
|
|
518
604
|
try {
|
|
519
605
|
const db = (this.store as any).db;
|
|
520
606
|
const total = db.prepare("SELECT COUNT(*) as count FROM chunks").get() as any;
|
|
@@ -530,9 +616,12 @@ export class ViewerServer {
|
|
|
530
616
|
}
|
|
531
617
|
let embCount = 0;
|
|
532
618
|
try { embCount = (db.prepare("SELECT COUNT(*) as count FROM embeddings").get() as any).count; } catch { /* table may not exist */ }
|
|
533
|
-
const
|
|
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
|
-
|
|
619
|
+
const sessionQuery = ownerFilter
|
|
620
|
+
? "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"
|
|
621
|
+
: "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";
|
|
622
|
+
const sessionList = (ownerFilter
|
|
623
|
+
? db.prepare(sessionQuery).all(ownerFilter)
|
|
624
|
+
: db.prepare(sessionQuery).all()) as any[];
|
|
536
625
|
|
|
537
626
|
let skillCount = 0;
|
|
538
627
|
try { skillCount = (db.prepare("SELECT COUNT(*) as count FROM skills").get() as any).count; } catch { /* table may not exist yet */ }
|
|
@@ -668,7 +757,12 @@ export class ViewerServer {
|
|
|
668
757
|
if (visibility) {
|
|
669
758
|
skills = skills.filter(s => s.visibility === visibility);
|
|
670
759
|
}
|
|
671
|
-
this.
|
|
760
|
+
const db = (this.store as any).db;
|
|
761
|
+
const enriched = skills.map(s => {
|
|
762
|
+
const hub = db.prepare("SELECT visibility FROM hub_skills WHERE source_skill_id = ? ORDER BY updated_at DESC LIMIT 1").get(s.id) as { visibility: string } | undefined;
|
|
763
|
+
return { ...s, sharingVisibility: hub?.visibility ?? null };
|
|
764
|
+
});
|
|
765
|
+
this.jsonResponse(res, { skills: enriched });
|
|
672
766
|
}
|
|
673
767
|
|
|
674
768
|
private serveSkillDetail(res: http.ServerResponse, urlPath: string): void {
|
|
@@ -684,8 +778,11 @@ export class ViewerServer {
|
|
|
684
778
|
const relatedTasks = this.store.getTasksBySkill(skillId);
|
|
685
779
|
const files = fs.existsSync(skill.dirPath) ? this.walkDir(skill.dirPath, skill.dirPath) : [];
|
|
686
780
|
|
|
781
|
+
const db = (this.store as any).db;
|
|
782
|
+
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;
|
|
783
|
+
|
|
687
784
|
this.jsonResponse(res, {
|
|
688
|
-
skill,
|
|
785
|
+
skill: { ...skill, sharingVisibility: sharedSkill?.visibility ?? null, sharingGroupId: sharedSkill?.group_id ?? null },
|
|
689
786
|
versions: versions.map(v => ({
|
|
690
787
|
id: v.id,
|
|
691
788
|
version: v.version,
|
|
@@ -796,7 +893,7 @@ export class ViewerServer {
|
|
|
796
893
|
private handleSkillVisibility(req: http.IncomingMessage, res: http.ServerResponse, urlPath: string): void {
|
|
797
894
|
const segments = urlPath.split("/");
|
|
798
895
|
const skillId = segments[segments.length - 2];
|
|
799
|
-
this.readBody(req, (body) => {
|
|
896
|
+
this.readBody(req, async (body) => {
|
|
800
897
|
try {
|
|
801
898
|
const parsed = JSON.parse(body);
|
|
802
899
|
const visibility = parsed.visibility;
|
|
@@ -812,7 +909,46 @@ export class ViewerServer {
|
|
|
812
909
|
return;
|
|
813
910
|
}
|
|
814
911
|
this.store.setSkillVisibility(skillId, visibility);
|
|
815
|
-
|
|
912
|
+
|
|
913
|
+
let hubSynced = false;
|
|
914
|
+
const sharing = this.ctx?.config?.sharing;
|
|
915
|
+
if (sharing?.enabled && this.ctx) {
|
|
916
|
+
try {
|
|
917
|
+
const hubClient = await this.resolveHubClientAware();
|
|
918
|
+
if (visibility === "public") {
|
|
919
|
+
const bundle = buildSkillBundleForHub(this.store, skillId);
|
|
920
|
+
const response = await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/publish", {
|
|
921
|
+
method: "POST",
|
|
922
|
+
body: JSON.stringify({ visibility: "public", groupId: null, metadata: bundle.metadata, bundle: bundle.bundle }),
|
|
923
|
+
}) as any;
|
|
924
|
+
if (hubClient.userId) {
|
|
925
|
+
const existing = this.store.getHubSkillBySource(hubClient.userId, skillId);
|
|
926
|
+
this.store.upsertHubSkill({
|
|
927
|
+
id: response?.skillId ?? existing?.id ?? crypto.randomUUID(),
|
|
928
|
+
sourceSkillId: skillId, sourceUserId: hubClient.userId,
|
|
929
|
+
name: skill.name, description: skill.description, version: skill.version,
|
|
930
|
+
groupId: null, visibility: "public",
|
|
931
|
+
bundle: JSON.stringify(bundle.bundle), qualityScore: skill.qualityScore,
|
|
932
|
+
createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(),
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
hubSynced = true;
|
|
936
|
+
this.log.info(`Skill "${skill.name}" published to Hub`);
|
|
937
|
+
} else {
|
|
938
|
+
await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/unpublish", {
|
|
939
|
+
method: "POST",
|
|
940
|
+
body: JSON.stringify({ sourceSkillId: skillId }),
|
|
941
|
+
});
|
|
942
|
+
if (hubClient.userId) this.store.deleteHubSkillBySource(hubClient.userId, skillId);
|
|
943
|
+
hubSynced = true;
|
|
944
|
+
this.log.info(`Skill "${skill.name}" unpublished from Hub`);
|
|
945
|
+
}
|
|
946
|
+
} catch (hubErr) {
|
|
947
|
+
this.log.warn(`Hub sync failed for skill visibility change: ${hubErr}`);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
this.jsonResponse(res, { ok: true, skillId, visibility, hubSynced });
|
|
816
952
|
} catch (err) {
|
|
817
953
|
const errMsg = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
|
|
818
954
|
this.log.error(`handleSkillVisibility error: skillId=${skillId}, body=${body}, err=${errMsg}`);
|
|
@@ -928,7 +1064,15 @@ export class ViewerServer {
|
|
|
928
1064
|
const cleaned = chunk.role === "user" && chunk.content
|
|
929
1065
|
? { ...chunk, content: stripInboundMetadata(chunk.content) }
|
|
930
1066
|
: chunk;
|
|
931
|
-
this.
|
|
1067
|
+
const localShared = this.store.getLocalSharedMemory(chunkId);
|
|
1068
|
+
this.jsonResponse(res, {
|
|
1069
|
+
memory: {
|
|
1070
|
+
...cleaned,
|
|
1071
|
+
localSharing: cleaned.owner === "public",
|
|
1072
|
+
localSharingManaged: !!localShared,
|
|
1073
|
+
localOriginalOwner: localShared?.originalOwner ?? null,
|
|
1074
|
+
},
|
|
1075
|
+
});
|
|
932
1076
|
}
|
|
933
1077
|
|
|
934
1078
|
private handleUpdate(req: http.IncomingMessage, res: http.ServerResponse, urlPath: string): void {
|
|
@@ -957,6 +1101,340 @@ export class ViewerServer {
|
|
|
957
1101
|
else { res.writeHead(404, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Not found" })); }
|
|
958
1102
|
}
|
|
959
1103
|
|
|
1104
|
+
private handleMemoryLocalShare(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
1105
|
+
this.readBody(req, (body) => {
|
|
1106
|
+
try {
|
|
1107
|
+
const parsed = JSON.parse(body || "{}");
|
|
1108
|
+
const chunkId = String(parsed.chunkId || "");
|
|
1109
|
+
if (!chunkId) return this.jsonResponse(res, { ok: false, error: "missing_chunk_id" }, 400);
|
|
1110
|
+
const result = this.store.markMemorySharedLocally(chunkId);
|
|
1111
|
+
if (!result.ok) {
|
|
1112
|
+
return this.jsonResponse(res, { ok: false, error: result.reason ?? "share_failed" }, result.reason === "not_found" ? 404 : 400);
|
|
1113
|
+
}
|
|
1114
|
+
this.jsonResponse(res, {
|
|
1115
|
+
ok: true,
|
|
1116
|
+
chunkId,
|
|
1117
|
+
owner: result.owner,
|
|
1118
|
+
localSharing: true,
|
|
1119
|
+
localSharingManaged: true,
|
|
1120
|
+
localOriginalOwner: result.originalOwner ?? null,
|
|
1121
|
+
});
|
|
1122
|
+
} catch (err) {
|
|
1123
|
+
this.jsonResponse(res, { ok: false, error: String(err) }, 400);
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
private handleMemoryLocalUnshare(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
1129
|
+
this.readBody(req, (body) => {
|
|
1130
|
+
try {
|
|
1131
|
+
const parsed = JSON.parse(body || "{}");
|
|
1132
|
+
const chunkId = String(parsed.chunkId || "");
|
|
1133
|
+
const privateOwner = typeof parsed.privateOwner === "string" ? parsed.privateOwner : undefined;
|
|
1134
|
+
if (!chunkId) return this.jsonResponse(res, { ok: false, error: "missing_chunk_id" }, 400);
|
|
1135
|
+
const result = this.store.unmarkMemorySharedLocally(chunkId, privateOwner);
|
|
1136
|
+
if (!result.ok) {
|
|
1137
|
+
return this.jsonResponse(res, { ok: false, error: result.reason ?? "unshare_failed" }, result.reason === "not_found" ? 404 : 400);
|
|
1138
|
+
}
|
|
1139
|
+
this.jsonResponse(res, {
|
|
1140
|
+
ok: true,
|
|
1141
|
+
chunkId,
|
|
1142
|
+
owner: result.owner,
|
|
1143
|
+
localSharing: false,
|
|
1144
|
+
localOriginalOwner: result.originalOwner ?? null,
|
|
1145
|
+
});
|
|
1146
|
+
} catch (err) {
|
|
1147
|
+
this.jsonResponse(res, { ok: false, error: String(err) }, 400);
|
|
1148
|
+
}
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// ─── Unified scope API ───
|
|
1153
|
+
|
|
1154
|
+
private handleMemoryScope(req: http.IncomingMessage, res: http.ServerResponse, urlPath: string): void {
|
|
1155
|
+
const chunkId = urlPath.split("/")[3];
|
|
1156
|
+
this.readBody(req, async (body) => {
|
|
1157
|
+
try {
|
|
1158
|
+
const parsed = JSON.parse(body || "{}");
|
|
1159
|
+
const scope = parsed.scope as string;
|
|
1160
|
+
if (!["private", "local", "team"].includes(scope)) {
|
|
1161
|
+
return this.jsonResponse(res, { ok: false, error: "scope must be 'private', 'local', or 'team'" }, 400);
|
|
1162
|
+
}
|
|
1163
|
+
const db = (this.store as any).db;
|
|
1164
|
+
const chunk = db.prepare("SELECT * FROM chunks WHERE id = ?").get(chunkId) as any;
|
|
1165
|
+
if (!chunk) return this.jsonResponse(res, { ok: false, error: "not_found" }, 404);
|
|
1166
|
+
|
|
1167
|
+
if (chunk.dedup_status && chunk.dedup_status !== "active") {
|
|
1168
|
+
return this.jsonResponse(res, { ok: false, error: "inactive_memory", message: "Merged/duplicate memories cannot be shared" }, 400);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
const isLocalShared = chunk.owner === "public";
|
|
1172
|
+
const hubMemory = this.getHubMemoryForChunk(chunkId);
|
|
1173
|
+
const isTeamShared = !!hubMemory;
|
|
1174
|
+
const currentScope = isTeamShared ? "team" : isLocalShared ? "local" : "private";
|
|
1175
|
+
|
|
1176
|
+
if (scope === currentScope) {
|
|
1177
|
+
return this.jsonResponse(res, { ok: true, scope, changed: false });
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
let hubSynced = false;
|
|
1181
|
+
|
|
1182
|
+
if (scope === "team") {
|
|
1183
|
+
if (!isLocalShared) this.store.markMemorySharedLocally(chunkId);
|
|
1184
|
+
if (!isTeamShared) {
|
|
1185
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1186
|
+
const refreshedChunk = db.prepare("SELECT * FROM chunks WHERE id = ?").get(chunkId) as any;
|
|
1187
|
+
const response = await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/share", {
|
|
1188
|
+
method: "POST",
|
|
1189
|
+
body: JSON.stringify({ memory: { sourceChunkId: refreshedChunk.id, role: refreshedChunk.role, content: refreshedChunk.content, summary: refreshedChunk.summary, kind: refreshedChunk.kind, groupId: null, visibility: "public" } }),
|
|
1190
|
+
});
|
|
1191
|
+
if (hubClient.userId) {
|
|
1192
|
+
const existing = this.store.getHubMemoryBySource(hubClient.userId, chunkId);
|
|
1193
|
+
this.store.upsertHubMemory({
|
|
1194
|
+
id: (response as any)?.memoryId ?? existing?.id ?? crypto.randomUUID(),
|
|
1195
|
+
sourceChunkId: chunkId, sourceUserId: hubClient.userId,
|
|
1196
|
+
role: refreshedChunk.role, content: refreshedChunk.content, summary: refreshedChunk.summary ?? "",
|
|
1197
|
+
kind: refreshedChunk.kind, groupId: null, visibility: "public",
|
|
1198
|
+
createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(),
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
hubSynced = true;
|
|
1202
|
+
}
|
|
1203
|
+
} else if (scope === "local") {
|
|
1204
|
+
if (isTeamShared) {
|
|
1205
|
+
try {
|
|
1206
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1207
|
+
await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/unshare", {
|
|
1208
|
+
method: "POST", body: JSON.stringify({ sourceChunkId: chunkId }),
|
|
1209
|
+
});
|
|
1210
|
+
if (hubClient.userId) this.store.deleteHubMemoryBySource(hubClient.userId, chunkId);
|
|
1211
|
+
hubSynced = true;
|
|
1212
|
+
} catch (err) { this.log.warn(`Failed to unshare memory from team: ${err}`); }
|
|
1213
|
+
}
|
|
1214
|
+
if (!isLocalShared) this.store.markMemorySharedLocally(chunkId);
|
|
1215
|
+
} else {
|
|
1216
|
+
if (isTeamShared) {
|
|
1217
|
+
try {
|
|
1218
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1219
|
+
await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/unshare", {
|
|
1220
|
+
method: "POST", body: JSON.stringify({ sourceChunkId: chunkId }),
|
|
1221
|
+
});
|
|
1222
|
+
if (hubClient.userId) this.store.deleteHubMemoryBySource(hubClient.userId, chunkId);
|
|
1223
|
+
hubSynced = true;
|
|
1224
|
+
} catch (err) { this.log.warn(`Failed to unshare memory from team: ${err}`); }
|
|
1225
|
+
}
|
|
1226
|
+
if (isLocalShared) this.store.unmarkMemorySharedLocally(chunkId);
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
this.jsonResponse(res, { ok: true, scope, changed: true, hubSynced });
|
|
1230
|
+
} catch (err) {
|
|
1231
|
+
this.jsonResponse(res, { ok: false, error: String(err) }, 500);
|
|
1232
|
+
}
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
private handleTaskScope(req: http.IncomingMessage, res: http.ServerResponse, urlPath: string): void {
|
|
1237
|
+
const taskId = urlPath.split("/")[3];
|
|
1238
|
+
this.readBody(req, async (body) => {
|
|
1239
|
+
try {
|
|
1240
|
+
const parsed = JSON.parse(body || "{}");
|
|
1241
|
+
const scope = parsed.scope as string;
|
|
1242
|
+
if (!["private", "local", "team"].includes(scope)) {
|
|
1243
|
+
return this.jsonResponse(res, { ok: false, error: "scope must be 'private', 'local', or 'team'" }, 400);
|
|
1244
|
+
}
|
|
1245
|
+
const task = this.store.getTask(taskId);
|
|
1246
|
+
if (!task) return this.jsonResponse(res, { ok: false, error: "task_not_found" }, 404);
|
|
1247
|
+
|
|
1248
|
+
if (scope !== "private" && task.status !== "completed") {
|
|
1249
|
+
return this.jsonResponse(res, { ok: false, error: "only_completed_tasks_can_be_shared" }, 400);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
const isLocalShared = task.owner === "public";
|
|
1253
|
+
const hubTask = this.getHubTaskForLocal(taskId);
|
|
1254
|
+
const isTeamShared = !!hubTask;
|
|
1255
|
+
const currentScope = isTeamShared ? "team" : isLocalShared ? "local" : "private";
|
|
1256
|
+
|
|
1257
|
+
if (scope === currentScope) {
|
|
1258
|
+
return this.jsonResponse(res, { ok: true, scope, changed: false });
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
let hubSynced = false;
|
|
1262
|
+
|
|
1263
|
+
if (scope === "local" || scope === "team") {
|
|
1264
|
+
if (!isLocalShared) {
|
|
1265
|
+
const originalOwner = task.owner;
|
|
1266
|
+
const db = (this.store as any).db;
|
|
1267
|
+
db.prepare("INSERT INTO local_shared_tasks (task_id, hub_task_id, original_owner, shared_at) VALUES (?, ?, ?, ?) ON CONFLICT(task_id) DO UPDATE SET original_owner = excluded.original_owner, shared_at = excluded.shared_at").run(taskId, "", originalOwner, Date.now());
|
|
1268
|
+
db.prepare("UPDATE tasks SET owner = 'public' WHERE id = ?").run(taskId);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
if (scope === "team") {
|
|
1273
|
+
if (!isTeamShared) {
|
|
1274
|
+
const chunks = this.store.getChunksByTask(taskId);
|
|
1275
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1276
|
+
const refreshedTask = this.store.getTask(taskId)!;
|
|
1277
|
+
const response = await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/share", {
|
|
1278
|
+
method: "POST",
|
|
1279
|
+
body: JSON.stringify({
|
|
1280
|
+
task: { id: refreshedTask.id, sourceTaskId: refreshedTask.id, title: refreshedTask.title, summary: refreshedTask.summary, groupId: null, visibility: "public", createdAt: refreshedTask.startedAt ?? Date.now(), updatedAt: refreshedTask.updatedAt ?? Date.now() },
|
|
1281
|
+
chunks: chunks.map((c) => ({ id: c.id, hubTaskId: refreshedTask.id, sourceTaskId: refreshedTask.id, sourceChunkId: c.id, role: c.role, content: c.content, summary: c.summary, kind: c.kind, groupId: null, visibility: "public", createdAt: c.createdAt ?? Date.now() })),
|
|
1282
|
+
}),
|
|
1283
|
+
});
|
|
1284
|
+
if (hubClient.userId) {
|
|
1285
|
+
const existing = this.store.getHubTaskBySource(hubClient.userId, taskId);
|
|
1286
|
+
this.store.upsertHubTask({
|
|
1287
|
+
id: (response as any)?.taskId ?? existing?.id ?? crypto.randomUUID(),
|
|
1288
|
+
sourceTaskId: taskId, sourceUserId: hubClient.userId, title: refreshedTask.title ?? "",
|
|
1289
|
+
summary: refreshedTask.summary ?? "", groupId: null, visibility: "public",
|
|
1290
|
+
createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(),
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
hubSynced = true;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
if (scope === "local" && isTeamShared) {
|
|
1298
|
+
try {
|
|
1299
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1300
|
+
await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/unshare", {
|
|
1301
|
+
method: "POST", body: JSON.stringify({ sourceTaskId: taskId }),
|
|
1302
|
+
});
|
|
1303
|
+
if (hubClient.userId) this.store.deleteHubTaskBySource(hubClient.userId, taskId);
|
|
1304
|
+
hubSynced = true;
|
|
1305
|
+
} catch (err) { this.log.warn(`Failed to unshare task from team: ${err}`); }
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
if (scope === "private") {
|
|
1309
|
+
if (isTeamShared) {
|
|
1310
|
+
try {
|
|
1311
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1312
|
+
await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/unshare", {
|
|
1313
|
+
method: "POST", body: JSON.stringify({ sourceTaskId: taskId }),
|
|
1314
|
+
});
|
|
1315
|
+
if (hubClient.userId) this.store.deleteHubTaskBySource(hubClient.userId, taskId);
|
|
1316
|
+
hubSynced = true;
|
|
1317
|
+
} catch (err) { this.log.warn(`Failed to unshare task from team: ${err}`); }
|
|
1318
|
+
}
|
|
1319
|
+
if (isLocalShared) {
|
|
1320
|
+
const db = (this.store as any).db;
|
|
1321
|
+
const shared = db.prepare("SELECT original_owner FROM local_shared_tasks WHERE task_id = ?").get(taskId) as any;
|
|
1322
|
+
const restoreOwner = shared?.original_owner ?? task.owner;
|
|
1323
|
+
if (restoreOwner && restoreOwner !== "public") {
|
|
1324
|
+
db.prepare("UPDATE tasks SET owner = ? WHERE id = ?").run(restoreOwner, taskId);
|
|
1325
|
+
}
|
|
1326
|
+
db.prepare("DELETE FROM local_shared_tasks WHERE task_id = ?").run(taskId);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
this.jsonResponse(res, { ok: true, scope, changed: true, hubSynced });
|
|
1331
|
+
} catch (err) {
|
|
1332
|
+
this.jsonResponse(res, { ok: false, error: String(err) }, 500);
|
|
1333
|
+
}
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
private handleSkillScope(req: http.IncomingMessage, res: http.ServerResponse, urlPath: string): void {
|
|
1338
|
+
const skillId = urlPath.split("/")[3];
|
|
1339
|
+
this.readBody(req, async (body) => {
|
|
1340
|
+
try {
|
|
1341
|
+
const parsed = JSON.parse(body || "{}");
|
|
1342
|
+
const scope = parsed.scope as string;
|
|
1343
|
+
if (!["private", "local", "team"].includes(scope)) {
|
|
1344
|
+
return this.jsonResponse(res, { ok: false, error: "scope must be 'private', 'local', or 'team'" }, 400);
|
|
1345
|
+
}
|
|
1346
|
+
const skill = this.store.getSkill(skillId);
|
|
1347
|
+
if (!skill) return this.jsonResponse(res, { ok: false, error: "skill_not_found" }, 404);
|
|
1348
|
+
|
|
1349
|
+
if (scope !== "private" && skill.status !== "active") {
|
|
1350
|
+
return this.jsonResponse(res, { ok: false, error: "only_active_skills_can_be_shared" }, 400);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
const isLocalShared = skill.visibility === "public";
|
|
1354
|
+
const hubSkill = this.getHubSkillForLocal(skillId);
|
|
1355
|
+
const isTeamShared = !!hubSkill;
|
|
1356
|
+
const currentScope = isTeamShared ? "team" : isLocalShared ? "local" : "private";
|
|
1357
|
+
|
|
1358
|
+
if (scope === currentScope) {
|
|
1359
|
+
return this.jsonResponse(res, { ok: true, scope, changed: false });
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
let hubSynced = false;
|
|
1363
|
+
|
|
1364
|
+
if (scope === "local" || scope === "team") {
|
|
1365
|
+
if (!isLocalShared) this.store.setSkillVisibility(skillId, "public");
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
if (scope === "team") {
|
|
1369
|
+
if (!isTeamShared) {
|
|
1370
|
+
const bundle = buildSkillBundleForHub(this.store, skillId);
|
|
1371
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1372
|
+
const response = await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/publish", {
|
|
1373
|
+
method: "POST",
|
|
1374
|
+
body: JSON.stringify({ visibility: "public", groupId: null, metadata: bundle.metadata, bundle: bundle.bundle }),
|
|
1375
|
+
});
|
|
1376
|
+
if (hubClient.userId) {
|
|
1377
|
+
const existing = this.store.getHubSkillBySource(hubClient.userId, skillId);
|
|
1378
|
+
this.store.upsertHubSkill({
|
|
1379
|
+
id: (response as any)?.skillId ?? existing?.id ?? crypto.randomUUID(),
|
|
1380
|
+
sourceSkillId: skillId, sourceUserId: hubClient.userId,
|
|
1381
|
+
name: skill.name, description: skill.description, version: skill.version,
|
|
1382
|
+
groupId: null, visibility: "public",
|
|
1383
|
+
bundle: JSON.stringify(bundle.bundle), qualityScore: skill.qualityScore,
|
|
1384
|
+
createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(),
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
hubSynced = true;
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
if (scope === "local" && isTeamShared) {
|
|
1392
|
+
try {
|
|
1393
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1394
|
+
await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/unpublish", {
|
|
1395
|
+
method: "POST", body: JSON.stringify({ sourceSkillId: skillId }),
|
|
1396
|
+
});
|
|
1397
|
+
if (hubClient.userId) this.store.deleteHubSkillBySource(hubClient.userId, skillId);
|
|
1398
|
+
hubSynced = true;
|
|
1399
|
+
} catch (err) { this.log.warn(`Failed to unpublish skill from team: ${err}`); }
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
if (scope === "private") {
|
|
1403
|
+
if (isTeamShared) {
|
|
1404
|
+
try {
|
|
1405
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1406
|
+
await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/unpublish", {
|
|
1407
|
+
method: "POST", body: JSON.stringify({ sourceSkillId: skillId }),
|
|
1408
|
+
});
|
|
1409
|
+
if (hubClient.userId) this.store.deleteHubSkillBySource(hubClient.userId, skillId);
|
|
1410
|
+
hubSynced = true;
|
|
1411
|
+
} catch (err) { this.log.warn(`Failed to unpublish skill from team: ${err}`); }
|
|
1412
|
+
}
|
|
1413
|
+
if (isLocalShared) this.store.setSkillVisibility(skillId, "private");
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
this.jsonResponse(res, { ok: true, scope, changed: true, hubSynced });
|
|
1417
|
+
} catch (err) {
|
|
1418
|
+
this.jsonResponse(res, { ok: false, error: String(err) }, 500);
|
|
1419
|
+
}
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
private getHubMemoryForChunk(chunkId: string): any {
|
|
1424
|
+
const db = (this.store as any).db;
|
|
1425
|
+
return db.prepare("SELECT * FROM hub_memories WHERE source_chunk_id = ? LIMIT 1").get(chunkId);
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
private getHubTaskForLocal(taskId: string): any {
|
|
1429
|
+
const db = (this.store as any).db;
|
|
1430
|
+
return db.prepare("SELECT * FROM hub_tasks WHERE source_task_id = ? LIMIT 1").get(taskId);
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
private getHubSkillForLocal(skillId: string): any {
|
|
1434
|
+
const db = (this.store as any).db;
|
|
1435
|
+
return db.prepare("SELECT * FROM hub_skills WHERE source_skill_id = ? LIMIT 1").get(skillId);
|
|
1436
|
+
}
|
|
1437
|
+
|
|
960
1438
|
private handleDeleteSession(res: http.ServerResponse, url: URL): void {
|
|
961
1439
|
const key = url.searchParams.get("key");
|
|
962
1440
|
if (!key) { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Missing key" })); return; }
|
|
@@ -992,8 +1470,844 @@ export class ViewerServer {
|
|
|
992
1470
|
|
|
993
1471
|
private getOpenClawConfigPath(): string {
|
|
994
1472
|
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
995
|
-
|
|
996
|
-
|
|
1473
|
+
return path.join(home, ".openclaw", "openclaw.json");
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
private getPluginEntryConfig(raw: any): Record<string, unknown> {
|
|
1477
|
+
const entries = raw?.plugins?.entries ?? {};
|
|
1478
|
+
return entries["memos-local-openclaw-plugin"]?.config
|
|
1479
|
+
?? entries["memos-lite-openclaw-plugin"]?.config
|
|
1480
|
+
?? entries["memos-lite"]?.config
|
|
1481
|
+
?? {};
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
private getResolvedViewerConfig(raw?: any): MemosLocalConfig {
|
|
1485
|
+
const pluginCfg = this.getPluginEntryConfig(raw);
|
|
1486
|
+
const stateDir = this.ctx?.stateDir ?? this.getOpenClawHome();
|
|
1487
|
+
return resolveConfig(pluginCfg as Partial<MemosLocalConfig>, stateDir);
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
private hasUsableEmbeddingProvider(cfg: MemosLocalConfig): boolean {
|
|
1491
|
+
const embedding = cfg.embedding;
|
|
1492
|
+
if (!embedding?.provider) return false;
|
|
1493
|
+
if (embedding.provider === "openclaw") {
|
|
1494
|
+
return !!(this.ctx?.openclawAPI) && embedding.capabilities?.hostEmbedding === true;
|
|
1495
|
+
}
|
|
1496
|
+
return true;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
private hasUsableSummarizerProvider(cfg: MemosLocalConfig): boolean {
|
|
1500
|
+
const summarizer = cfg.summarizer;
|
|
1501
|
+
if (!summarizer?.provider) return false;
|
|
1502
|
+
if (summarizer.provider === "openclaw") {
|
|
1503
|
+
return !!(this.ctx?.openclawAPI) && summarizer.capabilities?.hostCompletion === true;
|
|
1504
|
+
}
|
|
1505
|
+
return true;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
private async serveSharingStatus(res: http.ServerResponse): Promise<void> {
|
|
1509
|
+
const sharing = this.ctx?.config?.sharing;
|
|
1510
|
+
const persisted = this.store.getClientHubConnection();
|
|
1511
|
+
const resolvedHubUrl = sharing?.client?.hubAddress ? normalizeHubUrl(sharing.client.hubAddress) : persisted?.hubUrl ?? null;
|
|
1512
|
+
const hasClientConfig = Boolean(
|
|
1513
|
+
(sharing?.client?.hubAddress && sharing?.client?.userToken) ||
|
|
1514
|
+
(persisted?.hubUrl && persisted?.userToken),
|
|
1515
|
+
);
|
|
1516
|
+
const base = {
|
|
1517
|
+
enabled: Boolean(sharing?.enabled),
|
|
1518
|
+
role: sharing?.role ?? null,
|
|
1519
|
+
clientConfigured: hasClientConfig,
|
|
1520
|
+
hubUrl: resolvedHubUrl,
|
|
1521
|
+
connection: { connected: false, user: null as any, hubUrl: undefined as string | undefined, teamName: null as string | null, apiVersion: null as string | null },
|
|
1522
|
+
admin: { canManageUsers: false, rejectSupported: false },
|
|
1523
|
+
};
|
|
1524
|
+
|
|
1525
|
+
if (!this.ctx || !sharing?.enabled) {
|
|
1526
|
+
this.jsonResponse(res, base);
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
// Hub 模式下,本机就是管理者,直接赋予 admin 权限
|
|
1531
|
+
if (sharing.role === "hub") {
|
|
1532
|
+
base.admin.canManageUsers = true;
|
|
1533
|
+
base.admin.rejectSupported = true;
|
|
1534
|
+
base.connection.connected = true;
|
|
1535
|
+
base.connection.hubUrl = resolvedHubUrl ?? undefined;
|
|
1536
|
+
|
|
1537
|
+
let adminUser: any = { username: "hub-admin", role: "admin" };
|
|
1538
|
+
try {
|
|
1539
|
+
const hub = this.resolveHubConnection();
|
|
1540
|
+
if (hub) {
|
|
1541
|
+
const me = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/me", { method: "GET" }) as any;
|
|
1542
|
+
if (me) {
|
|
1543
|
+
adminUser = {
|
|
1544
|
+
id: me.id,
|
|
1545
|
+
username: me.username ?? "hub-admin",
|
|
1546
|
+
role: me.role ?? "admin",
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
} catch { /* fallback to default */ }
|
|
1551
|
+
base.connection.user = adminUser;
|
|
1552
|
+
|
|
1553
|
+
// Fetch team info from own hub
|
|
1554
|
+
try {
|
|
1555
|
+
const selfUrl = resolvedHubUrl || `http://localhost:${sharing.hub?.port ?? 21816}`;
|
|
1556
|
+
const info = await fetch(`${selfUrl}/api/v1/hub/info`).then(r => r.ok ? r.json() : null).catch(() => null) as any;
|
|
1557
|
+
base.connection.teamName = info?.teamName ?? sharing.hub?.teamName ?? null;
|
|
1558
|
+
base.connection.apiVersion = info?.apiVersion ?? null;
|
|
1559
|
+
} catch { /* ignore */ }
|
|
1560
|
+
this.jsonResponse(res, base);
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
const hasPendingConnection = Boolean(persisted?.hubUrl && persisted?.userId && !persisted?.userToken);
|
|
1565
|
+
if (!hasClientConfig && !hasPendingConnection) {
|
|
1566
|
+
this.jsonResponse(res, base);
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
try {
|
|
1571
|
+
const status = await getHubStatus(this.store, this.ctx.config);
|
|
1572
|
+
const output = { ...base, connection: { ...base.connection, ...status } } as any;
|
|
1573
|
+
if (status.user?.status === "pending") {
|
|
1574
|
+
output.connection.pendingApproval = true;
|
|
1575
|
+
}
|
|
1576
|
+
if (status.user?.status === "rejected") {
|
|
1577
|
+
output.connection.rejected = true;
|
|
1578
|
+
}
|
|
1579
|
+
if (status.connected && status.hubUrl) {
|
|
1580
|
+
try {
|
|
1581
|
+
const info = await fetch(`${status.hubUrl}/api/v1/hub/info`).then((r) => (r.ok ? r.json() : null)).catch(() => null) as any;
|
|
1582
|
+
output.connection.teamName = info?.teamName ?? null;
|
|
1583
|
+
output.connection.apiVersion = info?.apiVersion ?? null;
|
|
1584
|
+
} catch {}
|
|
1585
|
+
} else if (status.hubUrl) {
|
|
1586
|
+
try {
|
|
1587
|
+
const info = await fetch(`${status.hubUrl}/api/v1/hub/info`).then((r) => (r.ok ? r.json() : null)).catch(() => null) as any;
|
|
1588
|
+
output.connection.teamName = info?.teamName ?? null;
|
|
1589
|
+
} catch {}
|
|
1590
|
+
}
|
|
1591
|
+
output.admin.canManageUsers = status.connected && status.user?.role === "admin";
|
|
1592
|
+
output.admin.rejectSupported = output.admin.canManageUsers;
|
|
1593
|
+
this.jsonResponse(res, output);
|
|
1594
|
+
} catch (err) {
|
|
1595
|
+
this.jsonResponse(res, { ...base, error: String(err) });
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
private async serveSharingPendingUsers(res: http.ServerResponse): Promise<void> {
|
|
1600
|
+
if (!this.ctx) return this.jsonResponse(res, { users: [], error: "sharing_unavailable" });
|
|
1601
|
+
try {
|
|
1602
|
+
const hub = this.resolveHubConnection();
|
|
1603
|
+
if (!hub) return this.jsonResponse(res, { users: [], error: "not_configured" });
|
|
1604
|
+
const data = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/pending-users", { method: "GET" }) as any;
|
|
1605
|
+
this.jsonResponse(res, { users: Array.isArray(data?.users) ? data.users : [] });
|
|
1606
|
+
} catch (err) {
|
|
1607
|
+
this.jsonResponse(res, { users: [], error: String(err) });
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
private handleSharingApproveUser(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
1612
|
+
this.readBody(req, async (body) => {
|
|
1613
|
+
if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
1614
|
+
try {
|
|
1615
|
+
const parsed = JSON.parse(body || "{}");
|
|
1616
|
+
const hub = this.resolveHubConnection();
|
|
1617
|
+
if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1618
|
+
const result = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/approve-user", {
|
|
1619
|
+
method: "POST",
|
|
1620
|
+
body: JSON.stringify({ userId: parsed.userId, username: parsed.username }),
|
|
1621
|
+
});
|
|
1622
|
+
this.jsonResponse(res, { ok: true, result });
|
|
1623
|
+
} catch (err) {
|
|
1624
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1625
|
+
}
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
private handleSharingRejectUser(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
1630
|
+
this.readBody(req, async (body) => {
|
|
1631
|
+
if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
1632
|
+
try {
|
|
1633
|
+
const parsed = JSON.parse(body || "{}");
|
|
1634
|
+
const hub = this.resolveHubConnection();
|
|
1635
|
+
if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1636
|
+
const result = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/reject-user", {
|
|
1637
|
+
method: "POST",
|
|
1638
|
+
body: JSON.stringify({ userId: parsed.userId }),
|
|
1639
|
+
});
|
|
1640
|
+
this.jsonResponse(res, { ok: true, result });
|
|
1641
|
+
} catch (err) {
|
|
1642
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1643
|
+
}
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
private handleSharingChangeRole(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
1648
|
+
this.readBody(req, async (body) => {
|
|
1649
|
+
if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
1650
|
+
try {
|
|
1651
|
+
const parsed = JSON.parse(body || "{}");
|
|
1652
|
+
const hub = this.resolveHubConnection();
|
|
1653
|
+
if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1654
|
+
const result = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/change-role", {
|
|
1655
|
+
method: "POST",
|
|
1656
|
+
body: JSON.stringify({ userId: parsed.userId, role: parsed.role }),
|
|
1657
|
+
});
|
|
1658
|
+
this.jsonResponse(res, { ok: true, result });
|
|
1659
|
+
} catch (err) {
|
|
1660
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1661
|
+
}
|
|
1662
|
+
});
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
private handleSharingRemoveUser(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
1666
|
+
this.readBody(req, async (body) => {
|
|
1667
|
+
if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
1668
|
+
try {
|
|
1669
|
+
const parsed = JSON.parse(body || "{}");
|
|
1670
|
+
const hub = this.resolveHubConnection();
|
|
1671
|
+
if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1672
|
+
const result = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/remove-user", {
|
|
1673
|
+
method: "POST",
|
|
1674
|
+
body: JSON.stringify({ userId: parsed.userId, cleanResources: parsed.cleanResources === true }),
|
|
1675
|
+
});
|
|
1676
|
+
this.jsonResponse(res, { ok: true, result });
|
|
1677
|
+
} catch (err) {
|
|
1678
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1679
|
+
}
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
private handleAdminRenameUser(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
1684
|
+
this.readBody(req, async (body) => {
|
|
1685
|
+
if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
1686
|
+
try {
|
|
1687
|
+
const parsed = JSON.parse(body || "{}");
|
|
1688
|
+
const hub = this.resolveHubConnection();
|
|
1689
|
+
if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
1690
|
+
const result = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/rename-user", {
|
|
1691
|
+
method: "POST",
|
|
1692
|
+
body: JSON.stringify({ userId: parsed.userId, username: parsed.username }),
|
|
1693
|
+
});
|
|
1694
|
+
this.jsonResponse(res, { ok: true, result });
|
|
1695
|
+
} catch (err) {
|
|
1696
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1697
|
+
}
|
|
1698
|
+
});
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
private handleRetryJoin(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
1702
|
+
this.readBody(req, async (_body) => {
|
|
1703
|
+
if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
1704
|
+
const sharing = this.ctx.config.sharing;
|
|
1705
|
+
if (!sharing?.enabled || sharing.role !== "client") {
|
|
1706
|
+
return this.jsonResponse(res, { ok: false, error: "not_in_client_mode" });
|
|
1707
|
+
}
|
|
1708
|
+
const hubAddress = sharing.client?.hubAddress ?? "";
|
|
1709
|
+
const teamToken = sharing.client?.teamToken ?? "";
|
|
1710
|
+
if (!hubAddress || !teamToken) {
|
|
1711
|
+
return this.jsonResponse(res, { ok: false, error: "missing_hub_address_or_team_token" });
|
|
1712
|
+
}
|
|
1713
|
+
try {
|
|
1714
|
+
const hubUrl = normalizeHubUrl(hubAddress);
|
|
1715
|
+
const os = await import("os");
|
|
1716
|
+
const username = os.userInfo().username || "user";
|
|
1717
|
+
const hostname = os.hostname() || "unknown";
|
|
1718
|
+
const result = await hubRequestJson(hubUrl, "", "/api/v1/hub/join", {
|
|
1719
|
+
method: "POST",
|
|
1720
|
+
body: JSON.stringify({ teamToken, username, deviceName: hostname }),
|
|
1721
|
+
}) as any;
|
|
1722
|
+
this.store.setClientHubConnection({
|
|
1723
|
+
hubUrl,
|
|
1724
|
+
userId: String(result.userId || ""),
|
|
1725
|
+
username,
|
|
1726
|
+
userToken: result.userToken || "",
|
|
1727
|
+
role: "member",
|
|
1728
|
+
connectedAt: Date.now(),
|
|
1729
|
+
});
|
|
1730
|
+
this.jsonResponse(res, { ok: true, status: result.status || "pending" });
|
|
1731
|
+
} catch (err) {
|
|
1732
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1733
|
+
}
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
private async serveSharingMemoryList(res: http.ServerResponse, url: URL): Promise<void> {
|
|
1738
|
+
if (!this.ctx) return this.jsonResponse(res, { memories: [], error: "sharing_unavailable" });
|
|
1739
|
+
try {
|
|
1740
|
+
const limit = Number(url.searchParams.get("limit") || 40);
|
|
1741
|
+
const data = await hubListMemories(this.store, this.ctx, { limit });
|
|
1742
|
+
this.jsonResponse(res, { memories: Array.isArray(data?.memories) ? data.memories : [] });
|
|
1743
|
+
} catch (err) {
|
|
1744
|
+
this.jsonResponse(res, { memories: [], error: String(err) });
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
private async serveSharingTaskList(res: http.ServerResponse, url: URL): Promise<void> {
|
|
1749
|
+
if (!this.ctx) return this.jsonResponse(res, { tasks: [], error: "sharing_unavailable" });
|
|
1750
|
+
try {
|
|
1751
|
+
const limit = Number(url.searchParams.get("limit") || 40);
|
|
1752
|
+
const data = await hubListTasks(this.store, this.ctx, { limit });
|
|
1753
|
+
this.jsonResponse(res, { tasks: Array.isArray(data?.tasks) ? data.tasks : [] });
|
|
1754
|
+
} catch (err) {
|
|
1755
|
+
this.jsonResponse(res, { tasks: [], error: String(err) });
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
private async serveSharingSkillList(res: http.ServerResponse, url: URL): Promise<void> {
|
|
1760
|
+
if (!this.ctx) return this.jsonResponse(res, { skills: [], error: "sharing_unavailable" });
|
|
1761
|
+
try {
|
|
1762
|
+
const limit = Number(url.searchParams.get("limit") || 40);
|
|
1763
|
+
const data = await hubListSkills(this.store, this.ctx, { limit });
|
|
1764
|
+
this.jsonResponse(res, { skills: Array.isArray(data?.skills) ? data.skills : [] });
|
|
1765
|
+
} catch (err) {
|
|
1766
|
+
this.jsonResponse(res, { skills: [], error: String(err) });
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
private handleSharingMemorySearch(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
1771
|
+
this.readBody(req, async (body) => {
|
|
1772
|
+
if (!this.ctx) return this.jsonResponse(res, { local: { hits: [], meta: {} }, hub: { hits: [], meta: { totalCandidates: 0, searchedGroups: [], includedPublic: false } }, error: "sharing_unavailable" });
|
|
1773
|
+
const emptyHub = { hits: [], meta: { totalCandidates: 0, searchedGroups: [], includedPublic: false } };
|
|
1774
|
+
try {
|
|
1775
|
+
const parsed = JSON.parse(body || "{}");
|
|
1776
|
+
const query = String(parsed.query || "");
|
|
1777
|
+
const role = typeof parsed.role === "string" ? parsed.role : undefined;
|
|
1778
|
+
const maxResults = typeof parsed.maxResults === "number" ? parsed.maxResults : 10;
|
|
1779
|
+
const scope = parsed.scope === "group" || parsed.scope === "all" ? parsed.scope : "local";
|
|
1780
|
+
const local = this.searchLocalViewerMemories(query, { role, maxResults });
|
|
1781
|
+
if (scope === "local") {
|
|
1782
|
+
return this.jsonResponse(res, { local: { hits: local.hits, meta: local.meta }, hub: emptyHub });
|
|
1783
|
+
}
|
|
1784
|
+
try {
|
|
1785
|
+
const hub = await hubSearchMemories(this.store, this.ctx, { query, maxResults, scope });
|
|
1786
|
+
this.jsonResponse(res, { local: { hits: local.hits, meta: local.meta }, hub });
|
|
1787
|
+
} catch (err) {
|
|
1788
|
+
this.jsonResponse(res, { local: { hits: local.hits, meta: local.meta }, hub: emptyHub, error: String(err) });
|
|
1789
|
+
}
|
|
1790
|
+
} catch (err) {
|
|
1791
|
+
this.jsonResponse(res, { local: { hits: [], meta: {} }, hub: emptyHub, error: String(err) });
|
|
1792
|
+
}
|
|
1793
|
+
});
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
private handleSharingMemoryDetail(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
1797
|
+
this.readBody(req, async (body) => {
|
|
1798
|
+
if (!this.ctx) return this.jsonResponse(res, { error: "sharing_unavailable" });
|
|
1799
|
+
try {
|
|
1800
|
+
const parsed = JSON.parse(body || "{}");
|
|
1801
|
+
const detail = await hubGetMemoryDetail(this.store, this.ctx, { remoteHitId: String(parsed.remoteHitId || "") });
|
|
1802
|
+
this.jsonResponse(res, detail);
|
|
1803
|
+
} catch (err) {
|
|
1804
|
+
this.jsonResponse(res, { error: String(err) });
|
|
1805
|
+
}
|
|
1806
|
+
});
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
private async serveSharingSkillSearch(res: http.ServerResponse, url: URL): Promise<void> {
|
|
1810
|
+
if (!this.ctx) return this.jsonResponse(res, { local: { hits: [] }, hub: { hits: [] }, error: "sharing_unavailable" });
|
|
1811
|
+
try {
|
|
1812
|
+
const query = String(url.searchParams.get("query") || "");
|
|
1813
|
+
const scope = url.searchParams.get("scope") === "group" || url.searchParams.get("scope") === "all" ? url.searchParams.get("scope")! : "local";
|
|
1814
|
+
const recall = new RecallEngine(this.store, this.embedder, this.ctx);
|
|
1815
|
+
const localHits = await recall.searchSkills(query, "mix" as any, "agent:main");
|
|
1816
|
+
if (scope === "local") {
|
|
1817
|
+
return this.jsonResponse(res, { local: { hits: localHits }, hub: { hits: [] } });
|
|
1818
|
+
}
|
|
1819
|
+
try {
|
|
1820
|
+
const hub = await hubSearchSkills(this.store, this.ctx, { query, maxResults: Number(url.searchParams.get("maxResults") || 20) });
|
|
1821
|
+
this.jsonResponse(res, { local: { hits: localHits }, hub });
|
|
1822
|
+
} catch (err) {
|
|
1823
|
+
this.jsonResponse(res, { local: { hits: localHits }, hub: { hits: [] }, error: String(err) });
|
|
1824
|
+
}
|
|
1825
|
+
} catch (err) {
|
|
1826
|
+
this.jsonResponse(res, { local: { hits: [] }, hub: { hits: [] }, error: String(err) });
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
private searchLocalViewerMemories(query: string, options?: { role?: string; maxResults?: number }): { hits: any[]; meta: Record<string, unknown> } {
|
|
1831
|
+
const db = (this.store as any).db;
|
|
1832
|
+
const role = options?.role;
|
|
1833
|
+
const maxResults = options?.maxResults ?? 10;
|
|
1834
|
+
const params: any[] = [];
|
|
1835
|
+
let rows: any[] = [];
|
|
1836
|
+
try {
|
|
1837
|
+
let sql = "SELECT c.* FROM chunks_fts f JOIN chunks c ON f.rowid = c.rowid WHERE chunks_fts MATCH ?";
|
|
1838
|
+
params.push(query);
|
|
1839
|
+
if (role) {
|
|
1840
|
+
sql += " AND c.role = ?";
|
|
1841
|
+
params.push(role);
|
|
1842
|
+
}
|
|
1843
|
+
sql += " ORDER BY rank LIMIT ?";
|
|
1844
|
+
params.push(maxResults);
|
|
1845
|
+
rows = db.prepare(sql).all(...params);
|
|
1846
|
+
} catch {
|
|
1847
|
+
const likeParams: any[] = [`%${query}%`, `%${query}%`];
|
|
1848
|
+
let sql = "SELECT * FROM chunks WHERE (content LIKE ? OR summary LIKE ?)";
|
|
1849
|
+
if (role) {
|
|
1850
|
+
sql += " AND role = ?";
|
|
1851
|
+
likeParams.push(role);
|
|
1852
|
+
}
|
|
1853
|
+
sql += " ORDER BY created_at DESC LIMIT ?";
|
|
1854
|
+
likeParams.push(maxResults);
|
|
1855
|
+
rows = db.prepare(sql).all(...likeParams);
|
|
1856
|
+
}
|
|
1857
|
+
const hits = rows.map((row: any, idx: number) => ({
|
|
1858
|
+
id: row.id,
|
|
1859
|
+
summary: row.summary || row.content?.slice(0, 120) || "",
|
|
1860
|
+
excerpt: row.content || "",
|
|
1861
|
+
score: Math.max(0.3, 1 - idx * 0.1),
|
|
1862
|
+
role: row.role,
|
|
1863
|
+
ref: { sessionKey: row.session_key, chunkId: row.id, turnId: row.turn_id, seq: row.seq },
|
|
1864
|
+
taskId: row.task_id ?? null,
|
|
1865
|
+
skillId: row.skill_id ?? null,
|
|
1866
|
+
}));
|
|
1867
|
+
return { hits, meta: { total: hits.length, usedMaxResults: maxResults } };
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
private handleSharingTaskShare(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
1871
|
+
this.readBody(req, async (body) => {
|
|
1872
|
+
if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
1873
|
+
try {
|
|
1874
|
+
const parsed = JSON.parse(body || "{}");
|
|
1875
|
+
const taskId = String(parsed.taskId || "");
|
|
1876
|
+
const visibility = "public";
|
|
1877
|
+
const groupId: string | undefined = undefined;
|
|
1878
|
+
const task = this.store.getTask(taskId);
|
|
1879
|
+
if (!task) return this.jsonResponse(res, { ok: false, error: "task_not_found" });
|
|
1880
|
+
const chunks = this.store.getChunksByTask(taskId);
|
|
1881
|
+
if (chunks.length === 0) return this.jsonResponse(res, { ok: false, error: "no_chunks" });
|
|
1882
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1883
|
+
const response = await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/share", {
|
|
1884
|
+
method: "POST",
|
|
1885
|
+
body: JSON.stringify({
|
|
1886
|
+
task: {
|
|
1887
|
+
id: task.id,
|
|
1888
|
+
sourceTaskId: task.id,
|
|
1889
|
+
title: task.title,
|
|
1890
|
+
summary: task.summary,
|
|
1891
|
+
groupId: null,
|
|
1892
|
+
visibility,
|
|
1893
|
+
createdAt: task.startedAt ?? Date.now(),
|
|
1894
|
+
updatedAt: task.updatedAt ?? Date.now(),
|
|
1895
|
+
},
|
|
1896
|
+
chunks: chunks.map((chunk) => ({
|
|
1897
|
+
id: chunk.id,
|
|
1898
|
+
hubTaskId: task.id,
|
|
1899
|
+
sourceTaskId: task.id,
|
|
1900
|
+
sourceChunkId: chunk.id,
|
|
1901
|
+
role: chunk.role,
|
|
1902
|
+
content: chunk.content,
|
|
1903
|
+
summary: chunk.summary,
|
|
1904
|
+
kind: chunk.kind,
|
|
1905
|
+
createdAt: chunk.createdAt,
|
|
1906
|
+
})),
|
|
1907
|
+
}),
|
|
1908
|
+
});
|
|
1909
|
+
const hubUserId = hubClient.userId;
|
|
1910
|
+
if (hubUserId) {
|
|
1911
|
+
this.store.upsertHubTask({
|
|
1912
|
+
id: task.id,
|
|
1913
|
+
sourceTaskId: task.id,
|
|
1914
|
+
sourceUserId: hubUserId,
|
|
1915
|
+
title: task.title,
|
|
1916
|
+
summary: task.summary,
|
|
1917
|
+
groupId: null,
|
|
1918
|
+
visibility,
|
|
1919
|
+
createdAt: task.startedAt ?? Date.now(),
|
|
1920
|
+
updatedAt: task.updatedAt ?? Date.now(),
|
|
1921
|
+
});
|
|
1922
|
+
}
|
|
1923
|
+
this.jsonResponse(res, { ok: true, taskId, visibility, response });
|
|
1924
|
+
} catch (err) {
|
|
1925
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1926
|
+
}
|
|
1927
|
+
});
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
private handleSharingTaskUnshare(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
1931
|
+
this.readBody(req, async (body) => {
|
|
1932
|
+
if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
1933
|
+
try {
|
|
1934
|
+
const parsed = JSON.parse(body || "{}");
|
|
1935
|
+
const taskId = String(parsed.taskId || "");
|
|
1936
|
+
const task = this.store.getTask(taskId);
|
|
1937
|
+
if (!task) return this.jsonResponse(res, { ok: false, error: "task_not_found" });
|
|
1938
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1939
|
+
await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/unshare", {
|
|
1940
|
+
method: "POST",
|
|
1941
|
+
body: JSON.stringify({ sourceTaskId: task.id }),
|
|
1942
|
+
});
|
|
1943
|
+
const hubUserId = hubClient.userId;
|
|
1944
|
+
if (hubUserId) this.store.deleteHubTaskBySource(hubUserId, task.id);
|
|
1945
|
+
this.jsonResponse(res, { ok: true, taskId });
|
|
1946
|
+
} catch (err) {
|
|
1947
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1948
|
+
}
|
|
1949
|
+
});
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
private handleSharingMemoryShare(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
1953
|
+
this.readBody(req, async (body) => {
|
|
1954
|
+
if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
1955
|
+
try {
|
|
1956
|
+
const parsed = JSON.parse(body || "{}");
|
|
1957
|
+
const chunkId = String(parsed.chunkId || "");
|
|
1958
|
+
const visibility = "public";
|
|
1959
|
+
const groupId: string | undefined = undefined;
|
|
1960
|
+
const db = (this.store as any).db;
|
|
1961
|
+
const chunk = db.prepare("SELECT * FROM chunks WHERE id = ?").get(chunkId) as any;
|
|
1962
|
+
if (!chunk) return this.jsonResponse(res, { ok: false, error: "memory_not_found" });
|
|
1963
|
+
const hubClient = await this.resolveHubClientAware();
|
|
1964
|
+
const response = await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/share", {
|
|
1965
|
+
method: "POST",
|
|
1966
|
+
body: JSON.stringify({
|
|
1967
|
+
memory: {
|
|
1968
|
+
sourceChunkId: chunk.id,
|
|
1969
|
+
role: chunk.role,
|
|
1970
|
+
content: chunk.content,
|
|
1971
|
+
summary: chunk.summary,
|
|
1972
|
+
kind: chunk.kind,
|
|
1973
|
+
groupId: null,
|
|
1974
|
+
visibility,
|
|
1975
|
+
},
|
|
1976
|
+
}),
|
|
1977
|
+
});
|
|
1978
|
+
const hubUserId = hubClient.userId;
|
|
1979
|
+
if (hubUserId) {
|
|
1980
|
+
const now = Date.now();
|
|
1981
|
+
const existing = this.store.getHubMemoryBySource(hubUserId, chunk.id);
|
|
1982
|
+
this.store.upsertHubMemory({
|
|
1983
|
+
id: (response as any)?.memoryId ?? existing?.id ?? crypto.randomUUID(),
|
|
1984
|
+
sourceChunkId: chunk.id,
|
|
1985
|
+
sourceUserId: hubUserId,
|
|
1986
|
+
role: chunk.role,
|
|
1987
|
+
content: chunk.content,
|
|
1988
|
+
summary: chunk.summary ?? "",
|
|
1989
|
+
kind: chunk.kind,
|
|
1990
|
+
groupId: null,
|
|
1991
|
+
visibility,
|
|
1992
|
+
createdAt: existing?.createdAt ?? now,
|
|
1993
|
+
updatedAt: now,
|
|
1994
|
+
});
|
|
1995
|
+
}
|
|
1996
|
+
this.jsonResponse(res, { ok: true, chunkId, visibility, response });
|
|
1997
|
+
} catch (err) {
|
|
1998
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
1999
|
+
}
|
|
2000
|
+
});
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
private handleSharingMemoryUnshare(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
2004
|
+
this.readBody(req, async (body) => {
|
|
2005
|
+
if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
2006
|
+
try {
|
|
2007
|
+
const parsed = JSON.parse(body || "{}");
|
|
2008
|
+
const chunkId = String(parsed.chunkId || "");
|
|
2009
|
+
const hubClient = await this.resolveHubClientAware();
|
|
2010
|
+
await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/unshare", {
|
|
2011
|
+
method: "POST",
|
|
2012
|
+
body: JSON.stringify({ sourceChunkId: chunkId }),
|
|
2013
|
+
});
|
|
2014
|
+
const hubUserId = hubClient.userId;
|
|
2015
|
+
if (hubUserId) this.store.deleteHubMemoryBySource(hubUserId, chunkId);
|
|
2016
|
+
this.jsonResponse(res, { ok: true, chunkId });
|
|
2017
|
+
} catch (err) {
|
|
2018
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
2019
|
+
}
|
|
2020
|
+
});
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
private handleSharingSkillPull(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
2024
|
+
this.readBody(req, async (body) => {
|
|
2025
|
+
if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
2026
|
+
try {
|
|
2027
|
+
const parsed = JSON.parse(body || "{}");
|
|
2028
|
+
const skillId = String(parsed.skillId || "");
|
|
2029
|
+
const payload = await fetchHubSkillBundle(this.store, this.ctx, { skillId });
|
|
2030
|
+
const restored = restoreSkillBundleFromHub(this.store, this.ctx, payload);
|
|
2031
|
+
this.jsonResponse(res, { ok: true, pulled: true, hubSkillId: skillId, ...restored });
|
|
2032
|
+
} catch (err) {
|
|
2033
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
2034
|
+
}
|
|
2035
|
+
});
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
private handleSharingSkillShare(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
2039
|
+
this.readBody(req, async (body) => {
|
|
2040
|
+
if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
2041
|
+
try {
|
|
2042
|
+
const parsed = JSON.parse(body || "{}");
|
|
2043
|
+
const skillId = String(parsed.skillId || "");
|
|
2044
|
+
const visibility = "public";
|
|
2045
|
+
const groupId: string | null = null;
|
|
2046
|
+
const skill = this.store.getSkill(skillId);
|
|
2047
|
+
if (!skill) return this.jsonResponse(res, { ok: false, error: "skill_not_found" });
|
|
2048
|
+
const bundle = buildSkillBundleForHub(this.store, skillId);
|
|
2049
|
+
const hubClient = await this.resolveHubClientAware();
|
|
2050
|
+
const response = await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/publish", {
|
|
2051
|
+
method: "POST",
|
|
2052
|
+
body: JSON.stringify({
|
|
2053
|
+
visibility,
|
|
2054
|
+
groupId: null,
|
|
2055
|
+
metadata: bundle.metadata,
|
|
2056
|
+
bundle: bundle.bundle,
|
|
2057
|
+
}),
|
|
2058
|
+
});
|
|
2059
|
+
const hubUserId = hubClient.userId;
|
|
2060
|
+
if (hubUserId) {
|
|
2061
|
+
const existing = this.store.getHubSkillBySource(hubUserId, skillId);
|
|
2062
|
+
this.store.upsertHubSkill({
|
|
2063
|
+
id: (response as any)?.skillId ?? existing?.id ?? crypto.randomUUID(),
|
|
2064
|
+
sourceSkillId: skillId,
|
|
2065
|
+
sourceUserId: hubUserId,
|
|
2066
|
+
name: skill.name,
|
|
2067
|
+
description: skill.description,
|
|
2068
|
+
version: skill.version,
|
|
2069
|
+
groupId: null,
|
|
2070
|
+
visibility,
|
|
2071
|
+
bundle: JSON.stringify(bundle.bundle),
|
|
2072
|
+
qualityScore: skill.qualityScore,
|
|
2073
|
+
createdAt: existing?.createdAt ?? Date.now(),
|
|
2074
|
+
updatedAt: Date.now(),
|
|
2075
|
+
});
|
|
2076
|
+
}
|
|
2077
|
+
this.jsonResponse(res, { ok: true, skillId, visibility, response });
|
|
2078
|
+
} catch (err) {
|
|
2079
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
2080
|
+
}
|
|
2081
|
+
});
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
private handleSharingSkillUnshare(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
2085
|
+
this.readBody(req, async (body) => {
|
|
2086
|
+
if (!this.ctx) return this.jsonResponse(res, { ok: false, error: "sharing_unavailable" });
|
|
2087
|
+
try {
|
|
2088
|
+
const parsed = JSON.parse(body || "{}");
|
|
2089
|
+
const skillId = String(parsed.skillId || "");
|
|
2090
|
+
const skill = this.store.getSkill(skillId);
|
|
2091
|
+
if (!skill) return this.jsonResponse(res, { ok: false, error: "skill_not_found" });
|
|
2092
|
+
const hubClient = await this.resolveHubClientAware();
|
|
2093
|
+
await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/skills/unpublish", {
|
|
2094
|
+
method: "POST",
|
|
2095
|
+
body: JSON.stringify({ sourceSkillId: skill.id }),
|
|
2096
|
+
});
|
|
2097
|
+
const hubUserId = hubClient.userId;
|
|
2098
|
+
if (hubUserId) this.store.deleteHubSkillBySource(hubUserId, skill.id);
|
|
2099
|
+
this.jsonResponse(res, { ok: true, skillId });
|
|
2100
|
+
} catch (err) {
|
|
2101
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
2102
|
+
}
|
|
2103
|
+
});
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
private resolveHubConnection(): { hubUrl: string; userToken: string } | null {
|
|
2107
|
+
if (!this.ctx) return null;
|
|
2108
|
+
|
|
2109
|
+
// Hub 模式:连接自己,用 bootstrap admin token
|
|
2110
|
+
const sharing = this.ctx.config.sharing;
|
|
2111
|
+
if (sharing?.role === "hub") {
|
|
2112
|
+
const hubPort = sharing.hub?.port ?? 18800;
|
|
2113
|
+
const hubUrl = `http://127.0.0.1:${hubPort}`;
|
|
2114
|
+
try {
|
|
2115
|
+
const authPath = path.join(this.dataDir, "hub-auth.json");
|
|
2116
|
+
const authData = JSON.parse(fs.readFileSync(authPath, "utf8"));
|
|
2117
|
+
const adminToken = authData?.bootstrapAdminToken;
|
|
2118
|
+
if (adminToken) return { hubUrl, userToken: adminToken };
|
|
2119
|
+
} catch {
|
|
2120
|
+
// hub-auth.json 不存在或读取失败,fall through
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
// Client 模式:用配置的 hubAddress + userToken
|
|
2125
|
+
const conn = this.store.getClientHubConnection();
|
|
2126
|
+
const hubUrl = conn?.hubUrl || this.ctx.config.sharing?.client?.hubAddress || "";
|
|
2127
|
+
const userToken = conn?.userToken || this.ctx.config.sharing?.client?.userToken || "";
|
|
2128
|
+
if (!hubUrl || !userToken) return null;
|
|
2129
|
+
return { hubUrl: normalizeHubUrl(hubUrl), userToken };
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
/** resolveHubClient 的 viewer 版本:hub 模式下使用 bootstrap admin 身份 */
|
|
2133
|
+
private async resolveHubClientAware(): Promise<ResolvedHubClient> {
|
|
2134
|
+
if (!this.ctx) throw new Error("sharing_unavailable");
|
|
2135
|
+
const sharing = this.ctx.config.sharing;
|
|
2136
|
+
if (sharing?.role === "hub") {
|
|
2137
|
+
const hub = this.resolveHubConnection();
|
|
2138
|
+
if (hub) {
|
|
2139
|
+
const me = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/me", { method: "GET" }) as any;
|
|
2140
|
+
return {
|
|
2141
|
+
hubUrl: hub.hubUrl,
|
|
2142
|
+
userToken: hub.userToken,
|
|
2143
|
+
userId: String(me.id),
|
|
2144
|
+
username: String(me.username ?? "hub-admin"),
|
|
2145
|
+
role: String(me.role ?? "admin"),
|
|
2146
|
+
};
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
return resolveHubClient(this.store, this.ctx);
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
private async serveSharingUsers(res: http.ServerResponse): Promise<void> {
|
|
2153
|
+
const hub = this.resolveHubConnection();
|
|
2154
|
+
if (!hub) return this.jsonResponse(res, { users: [], error: "not_configured" });
|
|
2155
|
+
try {
|
|
2156
|
+
const data = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/users", { method: "GET" }) as any;
|
|
2157
|
+
this.jsonResponse(res, { users: Array.isArray(data?.users) ? data.users : [] });
|
|
2158
|
+
} catch (err) {
|
|
2159
|
+
this.jsonResponse(res, { users: [], error: String(err) });
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
// ─── Admin management endpoints (Hub-side data) ───
|
|
2164
|
+
|
|
2165
|
+
private async serveAdminSharedTasks(res: http.ServerResponse): Promise<void> {
|
|
2166
|
+
const hub = this.resolveHubConnection();
|
|
2167
|
+
if (!hub) return this.jsonResponse(res, { tasks: [], error: "not_configured" });
|
|
2168
|
+
try {
|
|
2169
|
+
const data = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/shared-tasks", { method: "GET" }) as any;
|
|
2170
|
+
const tasks = Array.isArray(data?.tasks) ? data.tasks : [];
|
|
2171
|
+
for (const tk of tasks) {
|
|
2172
|
+
if (!tk.summary && tk.sourceTaskId) {
|
|
2173
|
+
const local = this.store.getTask(tk.sourceTaskId);
|
|
2174
|
+
if (local) { tk.summary = local.summary; tk.title = tk.title || local.title; }
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
this.jsonResponse(res, { tasks });
|
|
2178
|
+
} catch (err) {
|
|
2179
|
+
this.jsonResponse(res, { tasks: [], error: String(err) });
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
private async handleAdminDeleteTask(res: http.ServerResponse, p: string): Promise<void> {
|
|
2184
|
+
const hub = this.resolveHubConnection();
|
|
2185
|
+
if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
2186
|
+
const taskId = decodeURIComponent(p.replace("/api/admin/shared-tasks/", ""));
|
|
2187
|
+
try {
|
|
2188
|
+
await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/admin/shared-tasks/${encodeURIComponent(taskId)}`, { method: "DELETE" });
|
|
2189
|
+
this.jsonResponse(res, { ok: true });
|
|
2190
|
+
} catch (err) {
|
|
2191
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
private async serveAdminSharedSkills(res: http.ServerResponse): Promise<void> {
|
|
2196
|
+
const hub = this.resolveHubConnection();
|
|
2197
|
+
if (!hub) return this.jsonResponse(res, { skills: [], error: "not_configured" });
|
|
2198
|
+
try {
|
|
2199
|
+
const data = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/shared-skills", { method: "GET" }) as any;
|
|
2200
|
+
const skills = Array.isArray(data?.skills) ? data.skills : [];
|
|
2201
|
+
for (const sk of skills) {
|
|
2202
|
+
if (!sk.description && sk.sourceSkillId) {
|
|
2203
|
+
const local = this.store.getSkill(sk.sourceSkillId);
|
|
2204
|
+
if (local) { sk.description = sk.description || local.description; sk.name = sk.name || local.name; }
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
this.jsonResponse(res, { skills });
|
|
2208
|
+
} catch (err) {
|
|
2209
|
+
this.jsonResponse(res, { skills: [], error: String(err) });
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
private async handleAdminDeleteSkill(res: http.ServerResponse, p: string): Promise<void> {
|
|
2214
|
+
const hub = this.resolveHubConnection();
|
|
2215
|
+
if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
2216
|
+
const skillId = decodeURIComponent(p.replace("/api/admin/shared-skills/", ""));
|
|
2217
|
+
try {
|
|
2218
|
+
await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/admin/shared-skills/${encodeURIComponent(skillId)}`, { method: "DELETE" });
|
|
2219
|
+
this.jsonResponse(res, { ok: true });
|
|
2220
|
+
} catch (err) {
|
|
2221
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
private async serveAdminSharedMemories(res: http.ServerResponse): Promise<void> {
|
|
2226
|
+
const hub = this.resolveHubConnection();
|
|
2227
|
+
if (!hub) return this.jsonResponse(res, { memories: [], error: "not_configured" });
|
|
2228
|
+
try {
|
|
2229
|
+
const data = await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/admin/shared-memories", { method: "GET" }) as any;
|
|
2230
|
+
const memories = Array.isArray(data?.memories) ? data.memories : [];
|
|
2231
|
+
for (const m of memories) {
|
|
2232
|
+
if (!m.content && m.sourceChunkId) {
|
|
2233
|
+
const local = this.store.getChunk(m.sourceChunkId);
|
|
2234
|
+
if (local) { m.content = local.content; if (!m.summary && local.summary) m.summary = local.summary; }
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
this.jsonResponse(res, { memories });
|
|
2238
|
+
} catch (err) {
|
|
2239
|
+
this.jsonResponse(res, { memories: [], error: String(err) });
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
private async handleAdminDeleteMemory(res: http.ServerResponse, p: string): Promise<void> {
|
|
2244
|
+
const hub = this.resolveHubConnection();
|
|
2245
|
+
if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
2246
|
+
const memoryId = decodeURIComponent(p.replace("/api/admin/shared-memories/", ""));
|
|
2247
|
+
try {
|
|
2248
|
+
await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/admin/shared-memories/${encodeURIComponent(memoryId)}`, { method: "DELETE" });
|
|
2249
|
+
this.jsonResponse(res, { ok: true });
|
|
2250
|
+
} catch (err) {
|
|
2251
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
private async serveSharingNotifications(res: http.ServerResponse, url: URL): Promise<void> {
|
|
2256
|
+
const hub = this.resolveHubConnection();
|
|
2257
|
+
if (!hub) return this.jsonResponse(res, { notifications: [], unreadCount: 0 });
|
|
2258
|
+
try {
|
|
2259
|
+
const unread = url.searchParams.get("unread") === "1" ? "?unread=1" : "";
|
|
2260
|
+
const data = await hubRequestJson(hub.hubUrl, hub.userToken, `/api/v1/hub/notifications${unread}`) as any;
|
|
2261
|
+
this.jsonResponse(res, data);
|
|
2262
|
+
} catch {
|
|
2263
|
+
this.jsonResponse(res, { notifications: [], unreadCount: 0 });
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
private handleSharingNotificationsRead(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
2268
|
+
const hub = this.resolveHubConnection();
|
|
2269
|
+
if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
2270
|
+
this.readBody(req, async (raw) => {
|
|
2271
|
+
try {
|
|
2272
|
+
const body = JSON.parse(raw || "{}");
|
|
2273
|
+
await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications/read", { method: "POST", body: JSON.stringify(body) });
|
|
2274
|
+
this.jsonResponse(res, { ok: true });
|
|
2275
|
+
} catch (err) {
|
|
2276
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
2277
|
+
}
|
|
2278
|
+
});
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
private handleSharingNotificationsClear(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
2282
|
+
const hub = this.resolveHubConnection();
|
|
2283
|
+
if (!hub) return this.jsonResponse(res, { ok: false, error: "not_configured" });
|
|
2284
|
+
this.readBody(req, async () => {
|
|
2285
|
+
try {
|
|
2286
|
+
await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications/clear", { method: "POST", body: "{}" });
|
|
2287
|
+
this.jsonResponse(res, { ok: true });
|
|
2288
|
+
} catch (err) {
|
|
2289
|
+
this.jsonResponse(res, { ok: false, error: String(err) });
|
|
2290
|
+
}
|
|
2291
|
+
});
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
private getLocalIPs(): string[] {
|
|
2295
|
+
const nets = os.networkInterfaces();
|
|
2296
|
+
const ips: string[] = [];
|
|
2297
|
+
for (const name of Object.keys(nets)) {
|
|
2298
|
+
for (const net of nets[name] ?? []) {
|
|
2299
|
+
if (net.family === "IPv4" && !net.internal) {
|
|
2300
|
+
ips.push(net.address);
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
return ips;
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
private serveLocalIPs(res: http.ServerResponse): void {
|
|
2308
|
+
const ips = this.getLocalIPs();
|
|
2309
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2310
|
+
res.end(JSON.stringify({ ips }));
|
|
997
2311
|
}
|
|
998
2312
|
|
|
999
2313
|
private serveConfig(res: http.ServerResponse): void {
|
|
@@ -1016,7 +2330,9 @@ export class ViewerServer {
|
|
|
1016
2330
|
?? entries["memos-lite-openclaw-plugin"]
|
|
1017
2331
|
?? entries["memos-lite"]
|
|
1018
2332
|
?? {};
|
|
1019
|
-
if (pluginEntry.viewerPort
|
|
2333
|
+
if ((pluginEntry as any).viewerPort != null) {
|
|
2334
|
+
result.viewerPort = (pluginEntry as any).viewerPort;
|
|
2335
|
+
} else if (topEntry.viewerPort) {
|
|
1020
2336
|
result.viewerPort = topEntry.viewerPort;
|
|
1021
2337
|
}
|
|
1022
2338
|
this.jsonResponse(res, result);
|
|
@@ -1055,6 +2371,35 @@ export class ViewerServer {
|
|
|
1055
2371
|
if (newCfg.skillEvolution) config.skillEvolution = newCfg.skillEvolution;
|
|
1056
2372
|
if (newCfg.viewerPort) config.viewerPort = newCfg.viewerPort;
|
|
1057
2373
|
if (newCfg.telemetry !== undefined) config.telemetry = newCfg.telemetry;
|
|
2374
|
+
if (newCfg.sharing !== undefined) {
|
|
2375
|
+
const existing = (config.sharing as Record<string, unknown>) || {};
|
|
2376
|
+
const merged = { ...existing, ...newCfg.sharing };
|
|
2377
|
+
if (newCfg.sharing.capabilities && existing.capabilities) {
|
|
2378
|
+
merged.capabilities = { ...(existing.capabilities as Record<string, unknown>), ...newCfg.sharing.capabilities };
|
|
2379
|
+
}
|
|
2380
|
+
if (merged.role === "client" && merged.client) {
|
|
2381
|
+
const clientCfg = merged.client as Record<string, unknown>;
|
|
2382
|
+
const addr = String(clientCfg.hubAddress || "");
|
|
2383
|
+
if (addr) {
|
|
2384
|
+
const localIPs = this.getLocalIPs();
|
|
2385
|
+
localIPs.push("127.0.0.1", "localhost", "0.0.0.0");
|
|
2386
|
+
try {
|
|
2387
|
+
const u = new URL(addr.startsWith("http") ? addr : `http://${addr}`);
|
|
2388
|
+
if (localIPs.includes(u.hostname)) {
|
|
2389
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2390
|
+
res.end(JSON.stringify({ error: "cannot_join_self" }));
|
|
2391
|
+
return;
|
|
2392
|
+
}
|
|
2393
|
+
} catch {}
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
if (merged.role === "hub") {
|
|
2397
|
+
merged.client = { hubAddress: "", userToken: "", teamToken: "" };
|
|
2398
|
+
} else if (merged.role === "client") {
|
|
2399
|
+
merged.hub = { port: 18800, teamName: "", teamToken: "" };
|
|
2400
|
+
}
|
|
2401
|
+
config.sharing = merged;
|
|
2402
|
+
}
|
|
1058
2403
|
|
|
1059
2404
|
fs.mkdirSync(path.dirname(cfgPath), { recursive: true });
|
|
1060
2405
|
fs.writeFileSync(cfgPath, JSON.stringify(raw, null, 2), "utf-8");
|
|
@@ -1068,6 +2413,88 @@ export class ViewerServer {
|
|
|
1068
2413
|
});
|
|
1069
2414
|
}
|
|
1070
2415
|
|
|
2416
|
+
private handleUpdateUsername(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
2417
|
+
this.readBody(req, async (body) => {
|
|
2418
|
+
if (!this.ctx) return this.jsonResponse(res, { error: "sharing_unavailable" });
|
|
2419
|
+
try {
|
|
2420
|
+
const { username } = JSON.parse(body || "{}");
|
|
2421
|
+
if (!username || typeof username !== "string" || username.trim().length < 2 || username.trim().length > 32) {
|
|
2422
|
+
return this.jsonResponse(res, { error: "invalid_username" }, 400);
|
|
2423
|
+
}
|
|
2424
|
+
const trimmed = username.trim();
|
|
2425
|
+
const hubClient = await this.resolveHubClientAware();
|
|
2426
|
+
const result = await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/me/update-profile", {
|
|
2427
|
+
method: "POST",
|
|
2428
|
+
body: JSON.stringify({ username: trimmed }),
|
|
2429
|
+
}) as any;
|
|
2430
|
+
if (result.ok && result.userToken) {
|
|
2431
|
+
const sharing = this.ctx.config.sharing;
|
|
2432
|
+
if (sharing?.role === "hub") {
|
|
2433
|
+
try {
|
|
2434
|
+
const authPath = path.join(this.dataDir, "hub-auth.json");
|
|
2435
|
+
const authData = JSON.parse(fs.readFileSync(authPath, "utf8"));
|
|
2436
|
+
authData.bootstrapAdminToken = result.userToken;
|
|
2437
|
+
fs.writeFileSync(authPath, JSON.stringify(authData, null, 2), "utf-8");
|
|
2438
|
+
this.log.info("hub-auth.json updated with new admin token after username change");
|
|
2439
|
+
} catch (e) {
|
|
2440
|
+
this.log.warn(`Failed to update hub-auth.json: ${e}`);
|
|
2441
|
+
}
|
|
2442
|
+
} else {
|
|
2443
|
+
const persisted = this.store.getClientHubConnection();
|
|
2444
|
+
if (persisted) {
|
|
2445
|
+
this.store.setClientHubConnection({
|
|
2446
|
+
...persisted,
|
|
2447
|
+
username: result.username,
|
|
2448
|
+
userToken: result.userToken,
|
|
2449
|
+
});
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
this.jsonResponse(res, result);
|
|
2454
|
+
} catch (err: any) {
|
|
2455
|
+
const msg = String(err?.message || err);
|
|
2456
|
+
if (msg.includes("409") || msg.includes("username_taken")) {
|
|
2457
|
+
return this.jsonResponse(res, { error: "username_taken" }, 409);
|
|
2458
|
+
}
|
|
2459
|
+
this.jsonResponse(res, { error: msg }, 500);
|
|
2460
|
+
}
|
|
2461
|
+
});
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
private handleTestHubConnection(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
2465
|
+
this.readBody(req, async (body) => {
|
|
2466
|
+
try {
|
|
2467
|
+
const { hubUrl } = JSON.parse(body);
|
|
2468
|
+
if (!hubUrl) { this.jsonResponse(res, { ok: false, error: "hubUrl is required" }); return; }
|
|
2469
|
+
try {
|
|
2470
|
+
const localIPs = this.getLocalIPs();
|
|
2471
|
+
localIPs.push("127.0.0.1", "localhost", "0.0.0.0");
|
|
2472
|
+
const parsed = new URL(hubUrl.startsWith("http") ? hubUrl : `http://${hubUrl}`);
|
|
2473
|
+
if (localIPs.includes(parsed.hostname)) {
|
|
2474
|
+
this.jsonResponse(res, { ok: false, error: "cannot_join_self" });
|
|
2475
|
+
return;
|
|
2476
|
+
}
|
|
2477
|
+
} catch {}
|
|
2478
|
+
const url = hubUrl.replace(/\/+$/, "") + "/api/v1/hub/info";
|
|
2479
|
+
const ctrl = new AbortController();
|
|
2480
|
+
const timeout = setTimeout(() => ctrl.abort(), 8000);
|
|
2481
|
+
try {
|
|
2482
|
+
const r = await fetch(url, { signal: ctrl.signal });
|
|
2483
|
+
clearTimeout(timeout);
|
|
2484
|
+
if (!r.ok) { this.jsonResponse(res, { ok: false, error: `HTTP ${r.status}` }); return; }
|
|
2485
|
+
const info = await r.json() as Record<string, unknown>;
|
|
2486
|
+
this.jsonResponse(res, { ok: true, teamName: info.teamName || "", apiVersion: info.apiVersion || "" });
|
|
2487
|
+
} catch (e: unknown) {
|
|
2488
|
+
clearTimeout(timeout);
|
|
2489
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2490
|
+
this.jsonResponse(res, { ok: false, error: msg.includes("abort") ? "Connection timeout (8s)" : msg });
|
|
2491
|
+
}
|
|
2492
|
+
} catch (e) {
|
|
2493
|
+
this.jsonResponse(res, { ok: false, error: String(e) });
|
|
2494
|
+
}
|
|
2495
|
+
});
|
|
2496
|
+
}
|
|
2497
|
+
|
|
1071
2498
|
private handleTestModel(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
1072
2499
|
this.readBody(req, async (body) => {
|
|
1073
2500
|
try {
|
|
@@ -1439,7 +2866,7 @@ export class ViewerServer {
|
|
|
1439
2866
|
|
|
1440
2867
|
private getOpenClawHome(): string {
|
|
1441
2868
|
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
1442
|
-
return
|
|
2869
|
+
return path.join(home, ".openclaw");
|
|
1443
2870
|
}
|
|
1444
2871
|
|
|
1445
2872
|
private handleCleanupPolluted(res: http.ServerResponse): void {
|
|
@@ -2366,8 +3793,8 @@ export class ViewerServer {
|
|
|
2366
3793
|
req.on("end", () => cb(body));
|
|
2367
3794
|
}
|
|
2368
3795
|
|
|
2369
|
-
private jsonResponse(res: http.ServerResponse, data: unknown): void {
|
|
2370
|
-
res.writeHead(
|
|
3796
|
+
private jsonResponse(res: http.ServerResponse, data: unknown, statusCode = 200): void {
|
|
3797
|
+
res.writeHead(statusCode, { "Content-Type": "application/json; charset=utf-8" });
|
|
2371
3798
|
res.end(JSON.stringify(data));
|
|
2372
3799
|
}
|
|
2373
3800
|
}
|