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